drag(drop(node))}>
{indents && indents.length
@@ -173,6 +182,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
return (
{
: null}
{
Clone
)}
+ {!isFolder && item.type === 'http-request' && (
+
{
+ e.stopPropagation();
+ dropdownTippyRef.current.hide();
+ setGenerateCodeItemModalOpen(true);
+ }}
+ >
+ Generate Code
+
+ )}
{
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
index b015df0a..b150ade8 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
@@ -16,7 +16,7 @@ import CollectionItem from './CollectionItem';
import RemoveCollection from './RemoveCollection';
import CollectionProperties from './CollectionProperties';
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
-import { isItemAFolder, isItemARequest, transformCollectionToSaveToIdb } from 'utils/collections';
+import { isItemAFolder, isItemARequest, transformCollectionToSaveToExportAsFile } from 'utils/collections';
import exportCollection from 'utils/collections/export';
import RenameCollection from './RenameCollection';
@@ -69,7 +69,7 @@ const Collection = ({ collection, searchText }) => {
const handleExportClick = () => {
const collectionCopy = cloneDeep(collection);
- exportCollection(transformCollectionToSaveToIdb(collectionCopy));
+ exportCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
};
const [{ isOver }, drop] = useDrop({
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/index.js b/packages/bruno-app/src/components/Sidebar/Collections/index.js
index 496f0145..af54350e 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/index.js
@@ -1,21 +1,61 @@
import React, { useState } from 'react';
-import { useSelector } from 'react-redux';
-import { IconSearch, IconFolders } from '@tabler/icons';
+import { useDispatch, useSelector } from 'react-redux';
+import {
+ IconSearch,
+ IconFolders,
+ IconArrowsSort,
+ IconSortAscendingLetters,
+ IconSortDescendingLetters
+} from '@tabler/icons';
import Collection from '../Collections/Collection';
import CreateCollection from '../CreateCollection';
import StyledWrapper from './StyledWrapper';
import CreateOrOpenCollection from './CreateOrOpenCollection';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
+import { sortCollections } from 'providers/ReduxStore/slices/collections/actions';
+// todo: move this to a separate folder
+// the coding convention is to keep all the components in a folder named after the component
const CollectionsBadge = () => {
+ const dispatch = useDispatch();
+ const { collections } = useSelector((state) => state.collections);
+ const { collectionSortOrder } = useSelector((state) => state.collections);
+ const sortCollectionOrder = () => {
+ let order;
+ switch (collectionSortOrder) {
+ case 'default':
+ order = 'alphabetical';
+ break;
+ case 'alphabetical':
+ order = 'reverseAlphabetical';
+ break;
+ case 'reverseAlphabetical':
+ order = 'default';
+ break;
+ }
+ dispatch(sortCollections({ order }));
+ };
return (
-
-
-
-
-
Collections
+
+
+
+
+
+ Collections
+
+ {collections.length >= 1 && (
+
sortCollectionOrder()}>
+ {collectionSortOrder == 'default' ? (
+
+ ) : collectionSortOrder == 'alphabetical' ? (
+
+ ) : (
+
+ )}
+
+ )}
);
diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
index 8a65bedb..bfe59ae8 100644
--- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
@@ -27,8 +27,9 @@ const CreateCollection = ({ onClose }) => {
collectionFolderName: Yup.string()
.min(1, 'must be atleast 1 characters')
.max(50, 'must be 50 characters or less')
+ .matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
.required('folder name is required'),
- collectionLocation: Yup.string().required('location is required')
+ collectionLocation: Yup.string().min(1, 'location is required').required('location is required')
}),
onSubmit: (values) => {
dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation))
@@ -43,7 +44,10 @@ const CreateCollection = ({ onClose }) => {
const browse = () => {
dispatch(browseDirectory())
.then((dirPath) => {
- formik.setFieldValue('collectionLocation', dirPath);
+ // When the user closes the diolog without selecting anything dirPath will be false
+ if (typeof dirPath === 'string') {
+ formik.setFieldValue('collectionLocation', dirPath);
+ }
})
.catch((error) => {
formik.setFieldValue('collectionLocation', '');
@@ -63,9 +67,8 @@ const CreateCollection = ({ onClose }) => {
diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js
index 217b7e36..9c8bbf4e 100644
--- a/packages/bruno-app/src/components/Sidebar/index.js
+++ b/packages/bruno-app/src/components/Sidebar/index.js
@@ -96,27 +96,16 @@ const Sidebar = () => {
/>
- {storedTheme === 'dark' ? (
-
- Star
-
- ) : (
-
- Star
-
- )}
+
+ Star
+
-
v0.16.3
+
v0.19.0
diff --git a/packages/bruno-app/src/components/SingleLineEditor/index.js b/packages/bruno-app/src/components/SingleLineEditor/index.js
index 5c4ba12d..bee59b2d 100644
--- a/packages/bruno-app/src/components/SingleLineEditor/index.js
+++ b/packages/bruno-app/src/components/SingleLineEditor/index.js
@@ -9,6 +9,40 @@ const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODE
if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
+ CodeMirror.registerHelper('hint', 'anyword', (editor, options) => {
+ const word = /[\w$-]+/;
+ const wordlist = (options && options.autocomplete) || [];
+ let cur = editor.getCursor(),
+ curLine = editor.getLine(cur.line);
+ let end = cur.ch,
+ start = end;
+ while (start && word.test(curLine.charAt(start - 1))) --start;
+ let curWord = start != end && curLine.slice(start, end);
+
+ // Check if curWord is a valid string before proceeding
+ if (typeof curWord !== 'string' || curWord.length < 3) {
+ return null; // Abort the hint
+ }
+
+ const list = (options && options.list) || [];
+ const re = new RegExp(word.source, 'g');
+ for (let dir = -1; dir <= 1; dir += 2) {
+ let line = cur.line,
+ endLine = Math.min(Math.max(line + dir * 500, editor.firstLine()), editor.lastLine()) + dir;
+ for (; line != endLine; line += dir) {
+ let text = editor.getLine(line),
+ m;
+ while ((m = re.exec(text))) {
+ if (line == cur.line && curWord.length < 3) continue;
+ list.push(...wordlist.filter((el) => el.toLowerCase().startsWith(curWord.toLowerCase())));
+ }
+ }
+ }
+ return { list: [...new Set(list)], from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end) };
+ });
+ CodeMirror.commands.autocomplete = (cm, hint, options) => {
+ cm.showHint({ hint, ...options });
+ };
}
class SingleLineEditor extends Component {
@@ -32,6 +66,7 @@ class SingleLineEditor extends Component {
variables: getAllVariables(this.props.collection)
},
scrollbarStyle: null,
+ tabindex: 0,
extraKeys: {
Enter: () => {
if (this.props.onRun) {
@@ -70,9 +105,19 @@ class SingleLineEditor extends Component {
},
'Cmd-F': () => {},
'Ctrl-F': () => {},
- Tab: () => {}
+ // Tabbing disabled to make tabindex work
+ Tab: false,
+ 'Shift-Tab': false
}
});
+ if (this.props.autocomplete) {
+ this.editor.on('keyup', (cm, event) => {
+ if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.keyCode != 13) {
+ /*Enter - do not open autocomplete list just after item has been selected in it*/
+ CodeMirror.commands.autocomplete(cm, CodeMirror.hint.anyword, { autocomplete: this.props.autocomplete });
+ }
+ });
+ }
this.editor.setValue(this.props.value || '');
this.editor.on('change', this._onEdit);
this.addOverlay();
diff --git a/packages/bruno-app/src/components/VariablesEditor/index.js b/packages/bruno-app/src/components/VariablesEditor/index.js
index c8fc36fa..735b9a54 100644
--- a/packages/bruno-app/src/components/VariablesEditor/index.js
+++ b/packages/bruno-app/src/components/VariablesEditor/index.js
@@ -87,7 +87,7 @@ const VariablesEditor = ({ collection }) => {
- Note: As of today, collection variables can only be set via the api -{' '}
+ Note: As of today, collection variables can only be set via the API -{' '}
getVar() and setVar() .
In the next release, we will add a UI to set and modify collection variables.
diff --git a/packages/bruno-app/src/components/Welcome/index.js b/packages/bruno-app/src/components/Welcome/index.js
index 625f18ab..27851653 100644
--- a/packages/bruno-app/src/components/Welcome/index.js
+++ b/packages/bruno-app/src/components/Welcome/index.js
@@ -54,7 +54,7 @@ const Welcome = () => {
diff --git a/packages/bruno-app/src/pages/ErrorBoundary/index.js b/packages/bruno-app/src/pages/ErrorBoundary/index.js
new file mode 100644
index 00000000..3b45122a
--- /dev/null
+++ b/packages/bruno-app/src/pages/ErrorBoundary/index.js
@@ -0,0 +1,44 @@
+import React from 'react';
+
+class ErrorBoundary extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = { hasError: false };
+ }
+ componentDidMount() {
+ // Add a global error event listener to capture client-side errors
+ window.onerror = (message, source, lineno, colno, error) => {
+ this.setState({ hasError: true, error });
+ };
+ }
+ componentDidCatch(error, errorInfo) {
+ console.log({ error, errorInfo });
+ }
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
+
Oops! Something went wrong
+
{this.state.error && this.state.error.toString()}
+ {this.state.error && this.state.error.stack && (
+
{this.state.error.stack}
+ )}
+
{
+ this.setState({ hasError: false, error: null });
+ }}
+ >
+ Close
+
+
+
+ );
+ }
+ return this.props.children;
+ }
+}
+
+export default ErrorBoundary;
diff --git a/packages/bruno-app/src/pages/_app.js b/packages/bruno-app/src/pages/_app.js
index 382b9509..ab269252 100644
--- a/packages/bruno-app/src/pages/_app.js
+++ b/packages/bruno-app/src/pages/_app.js
@@ -7,6 +7,7 @@ import { PreferencesProvider } from 'providers/Preferences';
import ReduxStore from 'providers/ReduxStore';
import ThemeProvider from 'providers/Theme/index';
+import ErrorBoundary from './ErrorBoundary';
import '../styles/app.scss';
import '../styles/globals.css';
@@ -41,23 +42,25 @@ function MyApp({ Component, pageProps }) {
}
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js
index a50e71df..522fa0d4 100644
--- a/packages/bruno-app/src/providers/Hotkeys/index.js
+++ b/packages/bruno-app/src/providers/Hotkeys/index.js
@@ -7,7 +7,6 @@ import SaveRequest from 'components/RequestPane/SaveRequest';
import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
import NetworkError from 'components/ResponsePane/NetworkError';
import NewRequest from 'components/Sidebar/NewRequest';
-import BrunoSupport from 'components/BrunoSupport';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
@@ -22,7 +21,6 @@ export const HotkeysProvider = (props) => {
const [showSaveRequestModal, setShowSaveRequestModal] = useState(false);
const [showEnvSettingsModal, setShowEnvSettingsModal] = useState(false);
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
- const [showBrunoSupportModal, setShowBrunoSupportModal] = useState(false);
const getCurrentCollectionItems = () => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
@@ -133,18 +131,6 @@ export const HotkeysProvider = (props) => {
};
}, [activeTabUid, tabs, collections, setShowNewRequestModal]);
- // help (ctrl/cmd + h)
- useEffect(() => {
- Mousetrap.bind(['command+h', 'ctrl+h'], (e) => {
- setShowBrunoSupportModal(true);
- return false; // this stops the event bubbling
- });
-
- return () => {
- Mousetrap.unbind(['command+h', 'ctrl+h']);
- };
- }, [setShowNewRequestModal]);
-
// close tab hotkey
useEffect(() => {
Mousetrap.bind(['command+w', 'ctrl+w'], (e) => {
@@ -164,7 +150,6 @@ export const HotkeysProvider = (props) => {
return (
- {showBrunoSupportModal && setShowBrunoSupportModal(false)} />}
{showSaveRequestModal && (
setShowSaveRequestModal(false)} />
)}
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
index cfede2a4..d806a383 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -12,7 +12,6 @@ import {
getItemsToResequence,
moveCollectionItemToRootOfCollection,
findCollectionByUid,
- recursivelyGetAllItemUids,
transformRequestToSaveToFilesystem,
findParentItemInCollection,
findEnvironmentInCollection,
@@ -22,7 +21,7 @@ import {
} from 'utils/collections';
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
import { waitForNextTick } from 'utils/common';
-import { getDirectoryName } from 'utils/common/platform';
+import { getDirectoryName, isWindowsOS } from 'utils/common/platform';
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
import {
@@ -39,6 +38,7 @@ import {
createCollection as _createCollection,
renameCollection as _renameCollection,
removeCollection as _removeCollection,
+ sortCollections as _sortCollections,
collectionAddEnvFileEvent as _collectionAddEnvFileEvent
} from './index';
@@ -145,6 +145,11 @@ export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) =>
.catch((err) => console.log(err));
};
+// todo: this can be directly put inside the collections/index.js file
+// the coding convention is to put only actions that need ipc in this file
+export const sortCollections = (order) => (dispatch) => {
+ dispatch(_sortCollections(order));
+};
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -262,7 +267,19 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
}
const { ipcRenderer } = window;
- ipcRenderer.invoke('renderer:rename-item', item.pathname, newPathname, newName).then(resolve).catch(reject);
+ ipcRenderer
+ .invoke('renderer:rename-item', item.pathname, newPathname, newName)
+ .then(() => {
+ // In case of Mac and Linux, we get the unlinkDir and addDir IPC events from electron which takes care of updating the state
+ // But in windows we don't get those events, so we need to update the state manually
+ // This looks like an issue in our watcher library chokidar
+ // GH: https://github.com/usebruno/bruno/issues/251
+ if (isWindowsOS()) {
+ dispatch(_renameItem({ newName, itemUid, collectionUid }));
+ }
+ resolve();
+ })
+ .catch(reject);
});
};
@@ -346,7 +363,16 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
ipcRenderer
.invoke('renderer:delete-item', item.pathname, item.type)
- .then(() => resolve())
+ .then(() => {
+ // In case of Mac and Linux, we get the unlinkDir IPC event from electron which takes care of updating the state
+ // But in windows we don't get those events, so we need to update the state manually
+ // This looks like an issue in our watcher library chokidar
+ // GH: https://github.com/usebruno/bruno/issues/265
+ if (isWindowsOS()) {
+ dispatch(_deleteItem({ itemUid, collectionUid }));
+ }
+ resolve();
+ })
.catch((error) => reject(error));
}
return;
@@ -620,6 +646,37 @@ export const addEnvironment = (name, collectionUid) => (dispatch, getState) => {
});
};
+export const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, getState) => {
+ return new Promise((resolve, reject) => {
+ const state = getState();
+ const collection = findCollectionByUid(state.collections.collections, collectionUid);
+ if (!collection) {
+ return reject(new Error('Collection not found'));
+ }
+
+ const baseEnv = findEnvironmentInCollection(collection, baseEnvUid);
+ if (!collection) {
+ return reject(new Error('Environmnent not found'));
+ }
+
+ ipcRenderer
+ .invoke('renderer:copy-environment', collection.pathname, name, baseEnv.variables)
+ .then(
+ dispatch(
+ updateLastAction({
+ collectionUid,
+ lastAction: {
+ type: 'ADD_ENVIRONMENT',
+ payload: name
+ }
+ })
+ )
+ )
+ .then(resolve)
+ .catch(reject);
+ });
+};
+
export const renameEnvironment = (newName, environmentUid, collectionUid) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const state = getState();
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index 495989eb..2cb1bdea 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -28,7 +28,8 @@ import { getSubdirectoriesFromRoot, getDirectoryName } from 'utils/common/platfo
const PATH_SEPARATOR = path.sep;
const initialState = {
- collections: []
+ collections: [],
+ collectionSortOrder: 'default'
};
export const collectionsSlice = createSlice({
@@ -38,12 +39,12 @@ export const collectionsSlice = createSlice({
createCollection: (state, action) => {
const collectionUids = map(state.collections, (c) => c.uid);
const collection = action.payload;
-
// last action is used to track the last action performed on the collection
// this is optional
// this is used in scenarios where we want to know the last action performed on the collection
// and take some extra action based on that
// for example, when a env is created, we want to auto select it the env modal
+ collection.importedAt = new Date().getTime();
collection.lastAction = null;
collapseCollection(collection);
@@ -70,6 +71,20 @@ export const collectionsSlice = createSlice({
removeCollection: (state, action) => {
state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);
},
+ sortCollections: (state, action) => {
+ state.collectionSortOrder = action.payload.order;
+ switch (action.payload.order) {
+ case 'default':
+ state.collections = state.collections.sort((a, b) => a.importedAt - b.importedAt);
+ break;
+ case 'alphabetical':
+ state.collections = state.collections.sort((a, b) => a.name.localeCompare(b.name));
+ break;
+ case 'reverseAlphabetical':
+ state.collections = state.collections.sort((a, b) => b.name.localeCompare(a.name));
+ break;
+ }
+ },
updateLastAction: (state, action) => {
const { collectionUid, lastAction } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -307,6 +322,31 @@ export const collectionsSlice = createSlice({
}
}
},
+ updateAuth: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ const item = findItemInCollection(collection, action.payload.itemUid);
+
+ if (item && isItemARequest(item)) {
+ if (!item.draft) {
+ item.draft = cloneDeep(item);
+ }
+
+ item.draft.request.auth = item.draft.request.auth || {};
+ switch (action.payload.mode) {
+ case 'bearer':
+ item.draft.request.auth.mode = 'bearer';
+ item.draft.request.auth.bearer = action.payload.content;
+ break;
+ case 'basic':
+ item.draft.request.auth.mode = 'basic';
+ item.draft.request.auth.basic = action.payload.content;
+ break;
+ }
+ }
+ }
+ },
addQueryParam: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -563,6 +603,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);
@@ -1141,6 +1195,7 @@ export const {
brunoConfigUpdateEvent,
renameCollection,
removeCollection,
+ sortCollections,
updateLastAction,
collectionUnlinkEnvFileEvent,
saveEnvironment,
@@ -1158,6 +1213,7 @@ export const {
collectionClicked,
collectionFolderClicked,
requestUrlChanged,
+ updateAuth,
addQueryParam,
updateQueryParam,
deleteQueryParam,
@@ -1170,6 +1226,7 @@ export const {
addMultipartFormParam,
updateMultipartFormParam,
deleteMultipartFormParam,
+ updateRequestAuthMode,
updateRequestBodyMode,
updateRequestBody,
updateRequestGraphqlQuery,
diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js
index 122d1625..7a4ad64d 100644
--- a/packages/bruno-app/src/themes/dark.js
+++ b/packages/bruno-app/src/themes/dark.js
@@ -9,13 +9,20 @@ const darkTheme = {
green: 'rgb(11 178 126)',
danger: '#f06f57',
muted: '#9d9d9d',
- purple: '#cd56d6'
+ purple: '#cd56d6',
+ yellow: '#f59e0b'
},
bg: {
danger: '#d03544'
}
},
+ input: {
+ bg: 'rgb(65, 65, 65)',
+ border: 'rgb(65, 65, 65)',
+ focusBorder: 'rgb(65, 65, 65)'
+ },
+
variables: {
bg: 'rgb(48, 48, 49)',
diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js
index 846940cd..b014c2f3 100644
--- a/packages/bruno-app/src/themes/light.js
+++ b/packages/bruno-app/src/themes/light.js
@@ -9,13 +9,20 @@ const lightTheme = {
green: '#047857',
danger: 'rgb(185, 28, 28)',
muted: '#4b5563',
- purple: '#8e44ad'
+ purple: '#8e44ad',
+ yellow: '#d97706'
},
bg: {
danger: '#dc3545'
}
},
+ input: {
+ bg: 'white',
+ border: '#ccc',
+ focusBorder: '#8b8b8b'
+ },
+
menubar: {
bg: 'rgb(44, 44, 44)'
},
diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js
new file mode 100644
index 00000000..b48fbc3c
--- /dev/null
+++ b/packages/bruno-app/src/utils/codegenerator/har.js
@@ -0,0 +1,71 @@
+const createContentType = (mode) => {
+ switch (mode) {
+ case 'json':
+ return 'application/json';
+ case 'xml':
+ return 'application/xml';
+ case 'formUrlEncoded':
+ return 'application/x-www-form-urlencoded';
+ case 'multipartForm':
+ return 'multipart/form-data';
+ default:
+ return 'application/json';
+ }
+};
+
+const createHeaders = (headers, mode) => {
+ const contentType = createContentType(mode);
+ const headersArray = headers
+ .filter((header) => header.enabled)
+ .map((header) => {
+ return {
+ name: header.name,
+ value: header.value
+ };
+ });
+ const headerNames = headersArray.map((header) => header.name);
+ if (!headerNames.includes('Content-Type')) {
+ return [...headersArray, { name: 'Content-Type', value: contentType }];
+ }
+ return headersArray;
+};
+
+const createQuery = (queryParams = []) => {
+ return queryParams.map((param) => {
+ return {
+ name: param.name,
+ value: param.value
+ };
+ });
+};
+
+const createPostData = (body) => {
+ const contentType = createContentType(body.mode);
+ if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
+ return {
+ mimeType: contentType,
+ params: body[body.mode]
+ .filter((param) => param.enabled)
+ .map((param) => ({ name: param.name, value: param.value }))
+ };
+ } else {
+ return {
+ mimeType: contentType,
+ text: body[body.mode]
+ };
+ }
+};
+
+export const buildHarRequest = (request) => {
+ return {
+ method: request.method,
+ url: request.url,
+ httpVersion: 'HTTP/1.1',
+ cookies: [],
+ headers: createHeaders(request.headers, request.body.mode),
+ queryString: createQuery(request.params),
+ postData: createPostData(request.body),
+ headersSize: 0,
+ bodySize: 0
+ };
+};
diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
index 50f314da..d37e10bb 100644
--- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
+++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
@@ -66,8 +66,7 @@ if (!SERVER_RENDERED) {
if (target.nodeName !== 'SPAN' || state.hoverTimeout !== undefined) {
return;
}
-
- if (target.className !== 'cm-variable-valid') {
+ if (!target.classList.contains('cm-variable-valid')) {
return;
}
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 80fe41dd..bf0fe687 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -129,9 +129,11 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
if (draggedItemParent) {
+ draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq);
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
} else {
+ collection.items = sortBy(collection.items, (item) => item.seq);
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
}
@@ -143,10 +145,12 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
if (targetItemParent) {
+ targetItemParent.items = sortBy(targetItemParent.items, (item) => item.seq);
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
} else {
+ collection.items = sortBy(collection.items, (item) => item.seq);
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
@@ -203,7 +207,7 @@ export const getItemsToResequence = (parent, collection) => {
return itemsToResequence;
};
-export const transformCollectionToSaveToIdb = (collection, options = {}) => {
+export const transformCollectionToSaveToExportAsFile = (collection, options = {}) => {
const copyHeaders = (headers) => {
return map(headers, (header) => {
return {
@@ -281,6 +285,16 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded),
multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm)
},
+ auth: {
+ mode: get(si.draft.request, 'auth.mode', 'none'),
+ basic: {
+ username: get(si.draft.request, 'auth.basic.username', ''),
+ password: get(si.draft.request, 'auth.basic.password', '')
+ },
+ bearer: {
+ token: get(si.draft.request, 'auth.bearer.token', '')
+ }
+ },
script: si.draft.request.script,
vars: si.draft.request.vars,
assertions: si.draft.request.assertions,
@@ -303,6 +317,16 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
},
+ auth: {
+ mode: get(si.request, 'auth.mode', 'none'),
+ basic: {
+ username: get(si.request, 'auth.basic.username', ''),
+ password: get(si.request, 'auth.basic.password', '')
+ },
+ bearer: {
+ token: get(si.request, 'auth.bearer.token', '')
+ }
+ },
script: si.request.script,
vars: si.request.vars,
assertions: si.request.assertions,
@@ -351,6 +375,7 @@ export const transformRequestToSaveToFilesystem = (item) => {
url: _item.request.url,
params: [],
headers: [],
+ auth: _item.request.auth,
body: _item.request.body,
script: _item.request.script,
vars: _item.request.vars,
@@ -445,6 +470,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();
diff --git a/packages/bruno-app/src/utils/common/codemirror.js b/packages/bruno-app/src/utils/common/codemirror.js
index 59daee83..ce2f8be1 100644
--- a/packages/bruno-app/src/utils/common/codemirror.js
+++ b/packages/bruno-app/src/utils/common/codemirror.js
@@ -25,10 +25,11 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
stream.eat('}');
let found = pathFoundInVariables(word, variables);
if (found) {
- return 'variable-valid';
+ return 'variable-valid random-' + (Math.random() + 1).toString(36).substring(9);
} else {
- return 'variable-invalid';
+ return 'variable-invalid random-' + (Math.random() + 1).toString(36).substring(9);
}
+ // Random classname added so adjacent variables are not rendered in the same SPAN by CodeMirror.
}
word += ch;
}
@@ -41,3 +42,25 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay);
});
};
+
+export const getCodeMirrorModeBasedOnContentType = (contentType) => {
+ if (!contentType || typeof contentType !== 'string') {
+ return 'application/text';
+ }
+
+ if (contentType.includes('json')) {
+ return 'application/ld+json';
+ } else if (contentType.includes('xml')) {
+ return 'application/xml';
+ } else if (contentType.includes('html')) {
+ return 'application/html';
+ } else if (contentType.includes('text')) {
+ return 'application/text';
+ } else if (contentType.includes('application/edn')) {
+ return 'application/xml';
+ } else if (contentType.includes('yaml')) {
+ return 'application/yaml';
+ } else {
+ return 'application/text';
+ }
+};
diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js
index 204cae6b..992ec233 100644
--- a/packages/bruno-app/src/utils/common/index.js
+++ b/packages/bruno-app/src/utils/common/index.js
@@ -51,6 +51,17 @@ export const safeStringifyJSON = (obj, indent = false) => {
}
};
+export const safeParseXML = (str, options) => {
+ if (!str || !str.length || typeof str !== 'string') {
+ return str;
+ }
+ try {
+ return xmlFormat(str, options);
+ } catch (e) {
+ return str;
+ }
+};
+
// Remove any characters that are not alphanumeric, spaces, hyphens, or underscores
export const normalizeFileName = (name) => {
if (!name) {
@@ -76,18 +87,10 @@ export const getContentType = (headers) => {
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
return 'application/xml';
}
+
+ return contentType[0];
}
}
+
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;
-};
diff --git a/packages/bruno-app/src/utils/common/platform.js b/packages/bruno-app/src/utils/common/platform.js
index d144796e..e49a66ec 100644
--- a/packages/bruno-app/src/utils/common/platform.js
+++ b/packages/bruno-app/src/utils/common/platform.js
@@ -1,6 +1,7 @@
import trim from 'lodash/trim';
import path from 'path';
import slash from './slash';
+import platform from 'platform';
export const isElectron = () => {
if (!window) {
@@ -33,3 +34,10 @@ export const getDirectoryName = (pathname) => {
return path.dirname(pathname);
};
+
+export const isWindowsOS = () => {
+ const os = platform.os;
+ const osFamily = os.family.toLowerCase();
+
+ return osFamily.includes('windows');
+};
diff --git a/packages/bruno-app/src/utils/importers/insomnia-collection.js b/packages/bruno-app/src/utils/importers/insomnia-collection.js
index b0fd7195..b402e890 100644
--- a/packages/bruno-app/src/utils/importers/insomnia-collection.js
+++ b/packages/bruno-app/src/utils/importers/insomnia-collection.js
@@ -30,10 +30,23 @@ const parseGraphQL = (text) => {
}
};
-const transformInsomniaRequestItem = (request) => {
+const addSuffixToDuplicateName = (item, index, allItems) => {
+ // Check if the request name already exist and if so add a number suffix
+ const nameSuffix = allItems.reduce((nameSuffix, otherItem, otherIndex) => {
+ if (otherItem.name === item.name && otherIndex < index) {
+ nameSuffix++;
+ }
+ return nameSuffix;
+ }, 0);
+ return nameSuffix !== 0 ? `${item.name}_${nameSuffix}` : item.name;
+};
+
+const transformInsomniaRequestItem = (request, index, allRequests) => {
+ const name = addSuffixToDuplicateName(request, index, allRequests);
+
const brunoRequestItem = {
uid: uuid(),
- name: request.name,
+ name,
type: 'http-request',
request: {
url: request.url,
@@ -126,9 +139,7 @@ const parseInsomniaCollection = (data) => {
try {
const insomniaExport = JSON.parse(data);
const insomniaResources = get(insomniaExport, 'resources', []);
- const insomniaCollection = insomniaResources.find(
- (resource) => resource._type === 'workspace' && resource.scope === 'collection'
- );
+ const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace');
if (!insomniaCollection) {
reject(new BrunoError('Collection not found inside Insomnia export'));
@@ -145,14 +156,15 @@ const parseInsomniaCollection = (data) => {
resources.filter((resource) => resource._type === 'request_group' && resource.parentId === parentId) || [];
const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === parentId);
- const folders = requestGroups.map((folder) => {
+ const folders = requestGroups.map((folder, index, allFolder) => {
+ const name = addSuffixToDuplicateName(folder, index, allFolder);
const requests = resources.filter(
(resource) => resource._type === 'request' && resource.parentId === folder._id
);
return {
uid: uuid(),
- name: folder.name,
+ name,
type: 'folder',
items: createFolderStructure(resources, folder._id).concat(requests.map(transformInsomniaRequestItem))
};
diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js
index abc5583f..f4af7092 100644
--- a/packages/bruno-app/src/utils/network/index.js
+++ b/packages/bruno-app/src/utils/network/index.js
@@ -1,10 +1,8 @@
export const sendNetworkRequest = async (item, collection, environment, collectionVariables) => {
return new Promise((resolve, reject) => {
if (['http-request', 'graphql-request'].includes(item.type)) {
- const timeStart = Date.now();
sendHttpRequest(item, collection, environment, collectionVariables)
.then((response) => {
- const timeEnd = Date.now();
resolve({
state: 'success',
data: response.data,
@@ -12,7 +10,7 @@ export const sendNetworkRequest = async (item, collection, environment, collecti
size: response.headers['content-length'] || 0,
status: response.status,
statusText: response.statusText,
- duration: timeEnd - timeStart
+ duration: response.duration
});
})
.catch((err) => reject(err));
diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js
index b28cc019..7f5a8e82 100644
--- a/packages/bruno-app/src/utils/url/index.js
+++ b/packages/bruno-app/src/utils/url/index.js
@@ -53,3 +53,12 @@ export const splitOnFirst = (str, char) => {
return [str.slice(0, index), str.slice(index + 1)];
};
+
+export const isValidUrl = (url) => {
+ try {
+ new URL(url);
+ return true;
+ } catch (err) {
+ return false;
+ }
+};
diff --git a/packages/bruno-cli/changelog.md b/packages/bruno-cli/changelog.md
index 542d1bda..31b66ddc 100644
--- a/packages/bruno-cli/changelog.md
+++ b/packages/bruno-cli/changelog.md
@@ -1,5 +1,9 @@
# Changelog
+## 0.11.0
+
+- fix(#119) Support for Basic and Bearer Auth
+
## 0.10.1
- fix(#233) Fixed Issue related to content header parsing
diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json
index eb9fe0cb..014ecc29 100644
--- a/packages/bruno-cli/package.json
+++ b/packages/bruno-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@usebruno/cli",
- "version": "0.10.1",
+ "version": "0.11.0",
"license": "MIT",
"main": "src/index.js",
"bin": {
@@ -22,7 +22,7 @@
],
"dependencies": {
"@usebruno/js": "0.6.0",
- "@usebruno/lang": "0.4.0",
+ "@usebruno/lang": "0.5.0",
"axios": "^1.5.1",
"chai": "^4.3.7",
"chalk": "^3.0.0",
diff --git a/packages/bruno-cli/src/index.js b/packages/bruno-cli/src/index.js
index d54c6861..d9bc6655 100644
--- a/packages/bruno-cli/src/index.js
+++ b/packages/bruno-cli/src/index.js
@@ -20,7 +20,7 @@ const run = async () => {
.commandDir('commands')
.epilogue(CLI_EPILOGUE)
.usage('Usage: $0 [options]')
- .demandCommand(1, "Woof !! Let's play with some apis !!")
+ .demandCommand(1, "Woof !! Let's play with some APIs !!")
.help('h')
.alias('h', 'help');
};
diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js
index f4072dbe..b9261647 100644
--- a/packages/bruno-cli/src/runner/interpolate-vars.js
+++ b/packages/bruno-cli/src/runner/interpolate-vars.js
@@ -109,6 +109,18 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
}
}
+ // todo: we have things happening in two places w.r.t basic auth
+ // need to refactor this in the future
+ // the request.auth (basic auth) object gets set inside the prepare-request.js file
+ if (request.auth) {
+ const username = interpolate(request.auth.username) || '';
+ const password = interpolate(request.auth.password) || '';
+
+ // use auth header based approach and delete the request.auth object
+ request.headers['authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
+ delete request.auth;
+ }
+
return request;
};
diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js
index 1cab8a1c..e766d08e 100644
--- a/packages/bruno-cli/src/runner/prepare-request.js
+++ b/packages/bruno-cli/src/runner/prepare-request.js
@@ -18,6 +18,20 @@ const prepareRequest = (request) => {
headers: headers
};
+ // Authentication
+ if (request.auth) {
+ if (request.auth.mode === 'basic') {
+ axiosRequest.auth = {
+ username: get(request, 'auth.basic.username'),
+ password: get(request, 'auth.basic.password')
+ };
+ }
+
+ if (request.auth.mode === 'bearer') {
+ axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
+ }
+ }
+
request.body = request.body || {};
if (request.body.mode === 'json') {
diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js
index d293bd6e..aecdcbef 100644
--- a/packages/bruno-cli/src/runner/run-single-request.js
+++ b/packages/bruno-cli/src/runner/run-single-request.js
@@ -163,7 +163,7 @@ const runSingleRequest = async function (
// run assertions
let assertionResults = [];
const assertions = get(bruJson, 'request.assertions');
- if (assertions && assertions.length) {
+ if (assertions) {
const assertRuntime = new AssertRuntime();
assertionResults = assertRuntime.runAssertions(
assertions,
@@ -187,7 +187,7 @@ const runSingleRequest = async function (
// run tests
let testResults = [];
const testFile = get(bruJson, 'request.tests');
- if (testFile && testFile.length) {
+ if (typeof testFile === 'string') {
const testRuntime = new TestRuntime();
const result = await testRuntime.runTests(
testFile,
@@ -268,7 +268,7 @@ const runSingleRequest = async function (
// run assertions
let assertionResults = [];
const assertions = get(bruJson, 'request.assertions');
- if (assertions && assertions.length) {
+ if (assertions) {
const assertRuntime = new AssertRuntime();
assertionResults = assertRuntime.runAssertions(
assertions,
@@ -292,7 +292,7 @@ const runSingleRequest = async function (
// run tests
let testResults = [];
const testFile = get(bruJson, 'request.tests');
- if (testFile && testFile.length) {
+ if (typeof testFile === 'string') {
const testRuntime = new TestRuntime();
const result = await testRuntime.runTests(
testFile,
diff --git a/packages/bruno-cli/src/utils/bru.js b/packages/bruno-cli/src/utils/bru.js
index 1ba6f016..68410639 100644
--- a/packages/bruno-cli/src/utils/bru.js
+++ b/packages/bruno-cli/src/utils/bru.js
@@ -38,6 +38,7 @@ const bruToJson = (bru) => {
request: {
method: _.upperCase(_.get(json, 'http.method')),
url: _.get(json, 'http.url'),
+ auth: _.get(json, 'auth', {}),
params: _.get(json, 'query', []),
headers: _.get(json, 'headers', []),
body: _.get(json, 'body', {}),
@@ -49,6 +50,7 @@ const bruToJson = (bru) => {
};
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
+ transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none');
return transformedJson;
} catch (err) {
diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json
index 7011c4f5..b10e6786 100644
--- a/packages/bruno-electron/package.json
+++ b/packages/bruno-electron/package.json
@@ -1,5 +1,5 @@
{
- "version": "v0.16.3",
+ "version": "v0.19.0",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",
@@ -15,7 +15,7 @@
},
"dependencies": {
"@usebruno/js": "0.6.0",
- "@usebruno/lang": "0.4.0",
+ "@usebruno/lang": "0.5.0",
"@usebruno/schema": "0.5.0",
"about-window": "^1.15.2",
"axios": "^1.5.1",
diff --git a/packages/bruno-electron/src/about/256x256.png b/packages/bruno-electron/src/about/256x256.png
new file mode 100644
index 00000000..d1263e9c
Binary files /dev/null and b/packages/bruno-electron/src/about/256x256.png differ
diff --git a/packages/bruno-electron/src/about/about.css b/packages/bruno-electron/src/about/about.css
new file mode 100644
index 00000000..dd8b987d
--- /dev/null
+++ b/packages/bruno-electron/src/about/about.css
@@ -0,0 +1,8 @@
+.versions {
+ -webkit-user-select: text;
+ user-select: text;
+}
+.title {
+ -webkit-user-select: text;
+ user-select: text;
+}
diff --git a/packages/bruno-electron/src/app/menu-template.js b/packages/bruno-electron/src/app/menu-template.js
index 7e091129..2efd93cd 100644
--- a/packages/bruno-electron/src/app/menu-template.js
+++ b/packages/bruno-electron/src/app/menu-template.js
@@ -51,7 +51,8 @@ const template = [
click: () =>
openAboutWindow({
product_name: 'Bruno',
- icon_path: join(process.cwd(), '/resources/icons/png/256x256.png'),
+ icon_path: join(__dirname, '../about/256x256.png'),
+ css_path: join(__dirname, '../about/about.css'),
homepage: 'https://www.usebruno.com/',
package_json_dir: join(__dirname, '../..')
})
diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js
index 45b10004..a28c04a7 100644
--- a/packages/bruno-electron/src/bru/index.js
+++ b/packages/bruno-electron/src/bru/index.js
@@ -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: {
diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js
index 2a62dd96..fb7fc45b 100644
--- a/packages/bruno-electron/src/index.js
+++ b/packages/bruno-electron/src/index.js
@@ -16,9 +16,7 @@ setContentSecurityPolicy(`
default-src * 'unsafe-inline' 'unsafe-eval';
script-src * 'unsafe-inline' 'unsafe-eval';
connect-src * 'unsafe-inline';
- base-uri 'none';
form-action 'none';
- img-src 'self' data:image/svg+xml;
`);
const menu = Menu.buildFromTemplate(menuTemplate);
@@ -35,7 +33,8 @@ app.on('ready', async () => {
webPreferences: {
nodeIntegration: true,
contextIsolation: true,
- preload: path.join(__dirname, 'preload.js')
+ preload: path.join(__dirname, 'preload.js'),
+ webviewTag: true
}
});
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index ae85558a..864aff82 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -151,6 +151,28 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
+ // copy environment
+ ipcMain.handle('renderer:copy-environment', async (event, collectionPathname, name, baseVariables) => {
+ try {
+ const envDirPath = path.join(collectionPathname, 'environments');
+ if (!fs.existsSync(envDirPath)) {
+ await createDirectory(envDirPath);
+ }
+
+ const envFilePath = path.join(envDirPath, `${name}.bru`);
+ if (fs.existsSync(envFilePath)) {
+ throw new Error(`environment: ${envFilePath} already exists`);
+ }
+
+ const content = envJsonToBru({
+ variables: baseVariables
+ });
+ await writeFile(envFilePath, content);
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
// save environment
ipcMain.handle('renderer:save-environment', async (event, collectionPathname, environment) => {
try {
diff --git a/packages/bruno-electron/src/ipc/network/axios-instance.js b/packages/bruno-electron/src/ipc/network/axios-instance.js
new file mode 100644
index 00000000..f4abd839
--- /dev/null
+++ b/packages/bruno-electron/src/ipc/network/axios-instance.js
@@ -0,0 +1,38 @@
+const axios = require('axios');
+
+/**
+ * Function that configures axios with timing interceptors
+ * Important to note here that the timings are not completely accurate.
+ * @see https://github.com/axios/axios/issues/695
+ * @returns {import('axios').AxiosStatic}
+ */
+function makeAxiosInstance() {
+ /** @type {import('axios').AxiosStatic} */
+ const instance = axios.create();
+
+ instance.interceptors.request.use((config) => {
+ config.headers['request-start-time'] = Date.now();
+ return config;
+ });
+
+ instance.interceptors.response.use(
+ (response) => {
+ const end = Date.now();
+ const start = response.config.headers['request-start-time'];
+ response.headers['request-duration'] = end - start;
+ return response;
+ },
+ (error) => {
+ const end = Date.now();
+ const start = error.config.headers['request-start-time'];
+ error.response.headers['request-duration'] = end - start;
+ return Promise.reject(error);
+ }
+ );
+
+ return instance;
+}
+
+module.exports = {
+ makeAxiosInstance
+};
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index 574437f8..2295dbf3 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -17,6 +17,7 @@ const { getProcessEnvVars } = require('../../store/process-env');
const { getBrunoConfig } = require('../../store/bruno-config');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { HttpProxyAgent } = require('http-proxy-agent');
+const { makeAxiosInstance } = require('./axios-instance');
// override the default escape function to prevent escaping
Mustache.escape = function (value) {
@@ -105,6 +106,8 @@ const registerNetworkIpc = (mainWindow) => {
const request = prepareRequest(_request);
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
+ const brunoConfig = getBrunoConfig(collectionUid);
+ const allowScriptFilesystemAccess = get(brunoConfig, 'filesystemAccess.allow', false);
try {
// make axios work in node using form data
@@ -156,7 +159,8 @@ const registerNetworkIpc = (mainWindow) => {
collectionVariables,
collectionPath,
onConsoleLog,
- processEnvVars
+ processEnvVars,
+ allowScriptFilesystemAccess
);
mainWindow.webContents.send('main:script-environment-update', {
@@ -242,7 +246,10 @@ const registerNetworkIpc = (mainWindow) => {
});
}
- const response = await axios(request);
+ const axiosInstance = makeAxiosInstance();
+
+ /** @type {import('axios').AxiosResponse} */
+ const response = await axiosInstance(request);
// run post-response vars
const postResponseVars = get(request, 'vars.res', []);
@@ -280,7 +287,8 @@ const registerNetworkIpc = (mainWindow) => {
collectionVariables,
collectionPath,
onConsoleLog,
- processEnvVars
+ processEnvVars,
+ allowScriptFilesystemAccess
);
mainWindow.webContents.send('main:script-environment-update', {
@@ -293,7 +301,7 @@ const registerNetworkIpc = (mainWindow) => {
// run assertions
const assertions = get(request, 'assertions');
- if (assertions && assertions.length) {
+ if (assertions) {
const assertRuntime = new AssertRuntime();
const results = assertRuntime.runAssertions(
assertions,
@@ -315,7 +323,7 @@ const registerNetworkIpc = (mainWindow) => {
// run tests
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
- if (testFile && testFile.length) {
+ if (typeof testFile === 'string') {
const testRuntime = new TestRuntime();
const testResults = await testRuntime.runTests(
testFile,
@@ -325,7 +333,8 @@ const registerNetworkIpc = (mainWindow) => {
collectionVariables,
collectionPath,
onConsoleLog,
- processEnvVars
+ processEnvVars,
+ allowScriptFilesystemAccess
);
mainWindow.webContents.send('main:run-request-event', {
@@ -345,12 +354,16 @@ const registerNetworkIpc = (mainWindow) => {
}
deleteCancelToken(cancelTokenUid);
+ // Prevents the duration on leaking to the actual result
+ const requestDuration = response.headers.get('request-duration');
+ response.headers.delete('request-duration');
return {
status: response.status,
statusText: response.statusText,
headers: response.headers,
- data: response.data
+ data: response.data,
+ duration: requestDuration
};
} catch (error) {
// todo: better error handling
@@ -367,7 +380,7 @@ const registerNetworkIpc = (mainWindow) => {
if (error && error.response) {
// run assertions
const assertions = get(request, 'assertions');
- if (assertions && assertions.length) {
+ if (assertions) {
const assertRuntime = new AssertRuntime();
const results = assertRuntime.runAssertions(
assertions,
@@ -389,7 +402,7 @@ const registerNetworkIpc = (mainWindow) => {
// run tests
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
- if (testFile && testFile.length) {
+ if (typeof testFile === 'string') {
const testRuntime = new TestRuntime();
const testResults = await testRuntime.runTests(
testFile,
@@ -399,7 +412,8 @@ const registerNetworkIpc = (mainWindow) => {
collectionVariables,
collectionPath,
onConsoleLog,
- processEnvVars
+ processEnvVars,
+ allowScriptFilesystemAccess
);
mainWindow.webContents.send('main:run-request-event', {
@@ -418,11 +432,15 @@ const registerNetworkIpc = (mainWindow) => {
});
}
+ // Prevents the duration from leaking to the actual result
+ const requestDuration = error.response.headers.get('request-duration');
+ error.response.headers.delete('request-duration');
return {
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
- data: error.response.data
+ data: error.response.data,
+ duration: requestDuration ?? 0
};
}
@@ -485,6 +503,8 @@ const registerNetworkIpc = (mainWindow) => {
const collectionUid = collection.uid;
const collectionPath = collection.pathname;
const folderUid = folder ? folder.uid : null;
+ const brunoConfig = getBrunoConfig(collectionUid);
+ const allowScriptFilesystemAccess = get(brunoConfig, 'filesystemAccess.allow', false);
const onConsoleLog = (type, args) => {
console[type](...args);
@@ -590,7 +610,8 @@ const registerNetworkIpc = (mainWindow) => {
collectionVariables,
collectionPath,
onConsoleLog,
- processEnvVars
+ processEnvVars,
+ allowScriptFilesystemAccess
);
mainWindow.webContents.send('main:script-environment-update', {
@@ -691,7 +712,8 @@ const registerNetworkIpc = (mainWindow) => {
collectionVariables,
collectionPath,
onConsoleLog,
- processEnvVars
+ processEnvVars,
+ allowScriptFilesystemAccess
);
mainWindow.webContents.send('main:script-environment-update', {
@@ -703,7 +725,7 @@ const registerNetworkIpc = (mainWindow) => {
// run assertions
const assertions = get(item, 'request.assertions');
- if (assertions && assertions.length) {
+ if (assertions) {
const assertRuntime = new AssertRuntime();
const results = assertRuntime.runAssertions(
assertions,
@@ -724,7 +746,7 @@ const registerNetworkIpc = (mainWindow) => {
// run tests
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
- if (testFile && testFile.length) {
+ if (typeof testFile === 'string') {
const testRuntime = new TestRuntime();
const testResults = await testRuntime.runTests(
testFile,
@@ -734,7 +756,8 @@ const registerNetworkIpc = (mainWindow) => {
collectionVariables,
collectionPath,
onConsoleLog,
- processEnvVars
+ processEnvVars,
+ allowScriptFilesystemAccess
);
mainWindow.webContents.send('main:run-folder-event', {
@@ -782,7 +805,7 @@ const registerNetworkIpc = (mainWindow) => {
// run assertions
const assertions = get(item, 'request.assertions');
- if (assertions && assertions.length) {
+ if (assertions) {
const assertRuntime = new AssertRuntime();
const results = assertRuntime.runAssertions(
assertions,
@@ -803,7 +826,7 @@ const registerNetworkIpc = (mainWindow) => {
// run tests
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
- if (testFile && testFile.length) {
+ if (typeof testFile === 'string') {
const testRuntime = new TestRuntime();
const testResults = await testRuntime.runTests(
testFile,
@@ -813,7 +836,8 @@ const registerNetworkIpc = (mainWindow) => {
collectionVariables,
collectionPath,
onConsoleLog,
- processEnvVars
+ processEnvVars,
+ allowScriptFilesystemAccess
);
mainWindow.webContents.send('main:run-folder-event', {
diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
index f4072dbe..b9261647 100644
--- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js
+++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
@@ -109,6 +109,18 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
}
}
+ // todo: we have things happening in two places w.r.t basic auth
+ // need to refactor this in the future
+ // the request.auth (basic auth) object gets set inside the prepare-request.js file
+ if (request.auth) {
+ const username = interpolate(request.auth.username) || '';
+ const password = interpolate(request.auth.password) || '';
+
+ // use auth header based approach and delete the request.auth object
+ request.headers['authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
+ delete request.auth;
+ }
+
return request;
};
diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js
index f07331c5..5a851291 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-request.js
@@ -18,6 +18,20 @@ const prepareRequest = (request) => {
headers: headers
};
+ // Authentication
+ if (request.auth) {
+ if (request.auth.mode === 'basic') {
+ axiosRequest.auth = {
+ username: get(request, 'auth.basic.username'),
+ password: get(request, 'auth.basic.password')
+ };
+ }
+
+ if (request.auth.mode === 'bearer') {
+ axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
+ }
+ }
+
if (request.body.mode === 'json') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/json';
diff --git a/packages/bruno-graphql-docs/rollup.config.js b/packages/bruno-graphql-docs/rollup.config.js
index dd2424c5..d289e1df 100644
--- a/packages/bruno-graphql-docs/rollup.config.js
+++ b/packages/bruno-graphql-docs/rollup.config.js
@@ -1,46 +1,48 @@
-const { nodeResolve } = require("@rollup/plugin-node-resolve");
-const commonjs = require("@rollup/plugin-commonjs");
-const typescript = require("@rollup/plugin-typescript");
-const dts = require("rollup-plugin-dts");
-const postcss = require("rollup-plugin-postcss");
-const { terser } = require("rollup-plugin-terser");
+const { nodeResolve } = require('@rollup/plugin-node-resolve');
+const commonjs = require('@rollup/plugin-commonjs');
+const typescript = require('@rollup/plugin-typescript');
+const dts = require('rollup-plugin-dts');
+const postcss = require('rollup-plugin-postcss');
+const { terser } = require('rollup-plugin-terser');
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
-const packageJson = require("./package.json");
+const packageJson = require('./package.json');
module.exports = [
{
- input: "src/index.ts",
+ input: 'src/index.ts',
output: [
{
file: packageJson.main,
- format: "cjs",
- sourcemap: true,
+ format: 'cjs',
+ sourcemap: true
},
{
file: packageJson.module,
- format: "esm",
- sourcemap: true,
- },
+ format: 'esm',
+ sourcemap: true
+ }
],
plugins: [
postcss({
minimize: true,
- extensions: ['.css']
+ extensions: ['.css'],
+ extract: true
}),
peerDepsExternal(),
nodeResolve({
extensions: ['.css']
}),
commonjs(),
- typescript({ tsconfig: "./tsconfig.json" }),
+ typescript({ tsconfig: './tsconfig.json' }),
terser()
],
- external: ["react", "react-dom", "index.css"]
+ external: ['react', 'react-dom', 'index.css']
},
{
- input: "dist/esm/index.d.ts",
- output: [{ file: "dist/index.d.ts", format: "esm" }],
- plugins: [dts.default()],
+ input: 'dist/esm/index.d.ts',
+ external: [/\.css$/],
+ output: [{ file: 'dist/index.d.ts', format: 'esm' }],
+ plugins: [dts.default()]
}
-];
\ No newline at end of file
+];
diff --git a/packages/bruno-graphql-docs/src/index.ts b/packages/bruno-graphql-docs/src/index.ts
index e38befd3..b8c6cd03 100644
--- a/packages/bruno-graphql-docs/src/index.ts
+++ b/packages/bruno-graphql-docs/src/index.ts
@@ -1,6 +1,5 @@
import { DocExplorer } from './components/DocExplorer';
-// Todo: Rollup throws error
import './index.css';
export { DocExplorer };
diff --git a/packages/bruno-js/src/bru.js b/packages/bruno-js/src/bru.js
index 6eec791f..3cd9e8f5 100644
--- a/packages/bruno-js/src/bru.js
+++ b/packages/bruno-js/src/bru.js
@@ -2,10 +2,11 @@ const Handlebars = require('handlebars');
const { cloneDeep } = require('lodash');
class Bru {
- constructor(envVariables, collectionVariables, processEnvVars) {
+ constructor(envVariables, collectionVariables, processEnvVars, collectionPath) {
this.envVariables = envVariables;
this.collectionVariables = collectionVariables;
this.processEnvVars = cloneDeep(processEnvVars || {});
+ this.collectionPath = collectionPath;
}
_interpolateEnvVar = (str) => {
@@ -24,6 +25,10 @@ class Bru {
});
};
+ cwd() {
+ return this.collectionPath;
+ }
+
getEnvName() {
return this.envVariables.__name__;
}
diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js
index 41af51ee..46c88b0c 100644
--- a/packages/bruno-js/src/runtime/script-runtime.js
+++ b/packages/bruno-js/src/runtime/script-runtime.js
@@ -7,6 +7,7 @@ const util = require('util');
const zlib = require('zlib');
const url = require('url');
const punycode = require('punycode');
+const fs = require('fs');
const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const BrunoResponse = require('../bruno-response');
@@ -27,6 +28,8 @@ const CryptoJS = require('crypto-js');
class ScriptRuntime {
constructor() {}
+ // This approach is getting out of hand
+ // Need to refactor this to use a single arg (object) instead of 7
async runRequestScript(
script,
request,
@@ -34,9 +37,10 @@ class ScriptRuntime {
collectionVariables,
collectionPath,
onConsoleLog,
- processEnvVars
+ processEnvVars,
+ allowScriptFilesystemAccess
) {
- const bru = new Bru(envVariables, collectionVariables, processEnvVars);
+ const bru = new Bru(envVariables, collectionVariables, processEnvVars, collectionPath);
const req = new BrunoRequest(request);
const context = {
@@ -84,7 +88,8 @@ class ScriptRuntime {
axios,
chai,
'node-fetch': fetch,
- 'crypto-js': CryptoJS
+ 'crypto-js': CryptoJS,
+ fs: allowScriptFilesystemAccess ? fs : undefined
}
}
});
@@ -105,9 +110,10 @@ class ScriptRuntime {
collectionVariables,
collectionPath,
onConsoleLog,
- processEnvVars
+ processEnvVars,
+ allowScriptFilesystemAccess
) {
- const bru = new Bru(envVariables, collectionVariables, processEnvVars);
+ const bru = new Bru(envVariables, collectionVariables, processEnvVars, collectionPath);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);
@@ -138,6 +144,16 @@ class ScriptRuntime {
external: true,
root: [collectionPath],
mock: {
+ // node libs
+ path,
+ stream,
+ util,
+ url,
+ http,
+ https,
+ punycode,
+ zlib,
+ // 3rd party libs
atob,
btoa,
lodash,
@@ -146,7 +162,8 @@ class ScriptRuntime {
nanoid,
axios,
'node-fetch': fetch,
- 'crypto-js': CryptoJS
+ 'crypto-js': CryptoJS,
+ fs: allowScriptFilesystemAccess ? fs : undefined
}
}
});
diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js
index 47daccd6..efefb451 100644
--- a/packages/bruno-js/src/runtime/test-runtime.js
+++ b/packages/bruno-js/src/runtime/test-runtime.js
@@ -1,6 +1,14 @@
const { NodeVM } = require('vm2');
const chai = require('chai');
const path = require('path');
+const http = require('http');
+const https = require('https');
+const stream = require('stream');
+const util = require('util');
+const zlib = require('zlib');
+const url = require('url');
+const punycode = require('punycode');
+const fs = require('fs');
const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const BrunoResponse = require('../bruno-response');
@@ -29,9 +37,10 @@ class TestRuntime {
collectionVariables,
collectionPath,
onConsoleLog,
- processEnvVars
+ processEnvVars,
+ allowScriptFilesystemAccess
) {
- const bru = new Bru(envVariables, collectionVariables, processEnvVars);
+ const bru = new Bru(envVariables, collectionVariables, processEnvVars, collectionPath);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);
@@ -78,6 +87,16 @@ class TestRuntime {
external: true,
root: [collectionPath],
mock: {
+ // node libs
+ path,
+ stream,
+ util,
+ url,
+ http,
+ https,
+ punycode,
+ zlib,
+ // 3rd party libs
atob,
axios,
btoa,
@@ -86,7 +105,8 @@ class TestRuntime {
uuid,
nanoid,
chai,
- 'crypto-js': CryptoJS
+ 'crypto-js': CryptoJS,
+ fs: allowScriptFilesystemAccess ? fs : undefined
}
}
});
diff --git a/packages/bruno-lang/package.json b/packages/bruno-lang/package.json
index 91ad7688..fffde571 100644
--- a/packages/bruno-lang/package.json
+++ b/packages/bruno-lang/package.json
@@ -1,7 +1,7 @@
{
"name": "@usebruno/lang",
- "version": "0.4.0",
- "license" : "MIT",
+ "version": "0.5.0",
+ "license": "MIT",
"main": "src/index.js",
"files": [
"src",
diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index 2241ca55..576c58c2 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -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: {
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index 818d7c9c..8ef44d7a 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -13,7 +13,7 @@ const stripLastLine = (text) => {
};
const 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 = '';
@@ -34,6 +34,11 @@ const jsonToBru = (json) => {
body: ${http.body}`;
}
+ if (http.auth && http.auth.length) {
+ bru += `
+ auth: ${http.auth}`;
+ }
+
bru += `
}
@@ -82,6 +87,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)}
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru
index ae7318da..c4ae4b05 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.bru
+++ b/packages/bruno-lang/v2/tests/fixtures/request.bru
@@ -7,6 +7,7 @@ meta {
get {
url: https://api.textlocal.in/send
body: json
+ auth: bearer
}
query {
@@ -21,6 +22,15 @@ headers {
~transaction-id: {{transactionId}}
}
+auth:basic {
+ username: john
+ password: secret
+}
+
+auth:bearer {
+ token: 123
+}
+
body:json {
{
"hello": "world"
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json
index 867229de..7a00f5bb 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.json
+++ b/packages/bruno-lang/v2/tests/fixtures/request.json
@@ -7,7 +7,8 @@
"http": {
"method": "get",
"url": "https://api.textlocal.in/send",
- "body": "json"
+ "body": "json",
+ "auth": "bearer"
},
"query": [
{
@@ -43,6 +44,15 @@
"enabled": false
}
],
+ "auth": {
+ "basic": {
+ "username": "john",
+ "password": "secret"
+ },
+ "bearer": {
+ "token": "123"
+ }
+ },
"body": {
"json": "{\n \"hello\": \"world\"\n}",
"text": "This is a text body",
diff --git a/packages/bruno-lang/v2/tests/index.spec.js b/packages/bruno-lang/v2/tests/index.spec.js
index ecc97067..e753ee96 100644
--- a/packages/bruno-lang/v2/tests/index.spec.js
+++ b/packages/bruno-lang/v2/tests/index.spec.js
@@ -14,7 +14,7 @@ describe('bruToJson', () => {
});
describe('jsonToBru', () => {
- it('should parse the bru file', () => {
+ it('should parse the json file', () => {
const input = require('./fixtures/request.json');
const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'request.bru'), 'utf8');
const output = jsonToBru(input);
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index 7076175b..81cd2528 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -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(),
diff --git a/readme.md b/readme.md
index badd9430..82d70127 100644
--- a/readme.md
+++ b/readme.md
@@ -1,7 +1,7 @@
-### Bruno - Opensource IDE for exploring and testing api's.
+### Bruno - Opensource IDE for exploring and testing APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
@@ -10,38 +10,48 @@
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
-
Bruno is a new and innovative API client, aimed at revolutionizing the status quo represented by Postman and similar tools out there.
Bruno stores your collections directly in a folder on your filesystem. We use a plain text markup language, Bru, to save information about API requests.
-You can use git or any version control of your choice to collaborate over your api collections.
+You can use git or any version control of your choice to collaborate over your API collections.
+Bruno is offline-only. There are no plans to add cloud-sync to Bruno, ever. We value your data privacy and believe it should stay on your device. Read our long-term vision [here](https://github.com/usebruno/bruno/discussions/269)
![bruno](assets/images/landing-2.png)
### Run across multiple platforms 🖥️
+
![bruno](assets/images/run-anywhere.png)
### Collaborate via Git 👩💻🧑💻
+
Or any version control system of your choice
![bruno](assets/images/version-control.png)
### Website 📄
+
Please visit [here](https://www.usebruno.com) to checkout our website and download the app
### Documentation 📄
+
Please visit [here](https://docs.usebruno.com) for documentation
+### Support ❤️
+
+Woof! If you like project, hit that ⭐ button !!
+
+### Share Testimonials 📣
+
+If Bruno has helped you at work and your teams, please don't forget to share your [testimonials on our github discussion](https://github.com/usebruno/bruno/discussions/343)
+
### Contribute 👩💻🧑💻
+
I am happy that you are looking to improve bruno. Please checkout the [contributing guide](contributing.md)
Even if you are not able to make contributions via code, please don't hesitate to file bugs and feature requests that needs to be implemented to solve your use case.
-### Support ❤️
-Woof! If you like project, hit that ⭐ button !!
-
### Authors
@@ -51,9 +61,11 @@ Woof! If you like project, hit that ⭐ button !!
### Stay in touch 🌐
+
[Twitter](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
### License 📄
+
[MIT](license.md)