mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-05 02:18:32 +00:00
Compare commits
72 Commits
feature/su
...
v0.14.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d4f7e6a06 | ||
|
|
f9ed68843d | ||
|
|
d3a56fdc82 | ||
|
|
e6c3a5af4c | ||
|
|
cee8073bb7 | ||
|
|
69be52cf9e | ||
|
|
9e400085e3 | ||
|
|
b07bb67943 | ||
|
|
593210456a | ||
|
|
e328a4615e | ||
|
|
18afb73238 | ||
|
|
73b71e0829 | ||
|
|
4c25aa99aa | ||
|
|
23843b5d0a | ||
|
|
7fbd338fa6 | ||
|
|
4aeb5cf56d | ||
|
|
1ed39a5ea6 | ||
|
|
74f248782b | ||
|
|
99239e19b4 | ||
|
|
f46160e161 | ||
|
|
d0147778db | ||
|
|
87119cee2e | ||
|
|
b25d896dd6 | ||
|
|
08495e7fb5 | ||
|
|
ee084696f5 | ||
|
|
aeb9fc8875 | ||
|
|
26a05f92cb | ||
|
|
fff7819c46 | ||
|
|
1d678ee0d9 | ||
|
|
fd4c188c95 | ||
|
|
97678b05fc | ||
|
|
a1c9625aee | ||
|
|
a9d74467ff | ||
|
|
bd670eceb6 | ||
|
|
1ccb66e92a | ||
|
|
d62881fe0d | ||
|
|
9ea95b4571 | ||
|
|
df9322b767 | ||
|
|
c27c750c3e | ||
|
|
94baee8e25 | ||
|
|
417b50b0ad | ||
|
|
bf5ee7e409 | ||
|
|
3ca0107f1b | ||
|
|
ec22fdb637 | ||
|
|
66024d04e9 | ||
|
|
330a7ad18a | ||
|
|
2976842588 | ||
|
|
51e0ea2c2d | ||
|
|
3b85e7ebcc | ||
|
|
49b0f3a322 | ||
|
|
45ca5ded96 | ||
|
|
b6528062f0 | ||
|
|
86094cc054 | ||
|
|
8e0bc68ada | ||
|
|
c36c7b44a6 | ||
|
|
0ac27dee56 | ||
|
|
ede122ab09 | ||
|
|
96e368cb18 | ||
|
|
942b75861c | ||
|
|
c1711ea01b | ||
|
|
dedfefbc9a | ||
|
|
65dd5df87e | ||
|
|
78d2393686 | ||
|
|
0b65c4580e | ||
|
|
9cc1bf1e2f | ||
|
|
b96e3d0f23 | ||
|
|
11c99d55dc | ||
|
|
d054dc4c78 | ||
|
|
9014dc5769 | ||
|
|
d346970241 | ||
|
|
bdb3051c2b | ||
|
|
f8325b22b3 |
12
.github/workflows/bump-homebrew-cask.yml
vendored
Normal file
12
.github/workflows/bump-homebrew-cask.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: Bump Homebrew Cask
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
runs-on: macos-10.15
|
||||
steps:
|
||||
- name: Bump Homebrew Cask
|
||||
run: brew bump-cask-pr bruno --version "${GITHUB_REF_NAME#v}"
|
||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx pretty-quick --staged
|
||||
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 180
|
||||
}
|
||||
BIN
assets/images/cli-demo.png
Normal file
BIN
assets/images/cli-demo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
assets/images/run-anywhere.png
Normal file
BIN
assets/images/run-anywhere.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
BIN
assets/images/version-control.png
Normal file
BIN
assets/images/version-control.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
@@ -17,7 +17,9 @@
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.2.0",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"ts-jest": "^29.0.5"
|
||||
},
|
||||
@@ -30,9 +32,10 @@
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
"build:electron": "./scripts/build-electron.sh",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report"
|
||||
"test:report": "npx playwright show-report",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.text};
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useState } from 'react';
|
||||
import { usePreferences } from 'providers/Preferences';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const General = () => {
|
||||
const {
|
||||
preferences,
|
||||
setPreferences,
|
||||
} = usePreferences();
|
||||
|
||||
const [sslVerification, setSslVerification] = useState(
|
||||
preferences.request.sslVerification
|
||||
);
|
||||
|
||||
const handleCheckboxChange = () => {
|
||||
const updatedPreferences = {
|
||||
...preferences,
|
||||
request: {
|
||||
...preferences.request,
|
||||
sslVerification: !sslVerification,
|
||||
},
|
||||
};
|
||||
|
||||
setPreferences(updatedPreferences)
|
||||
.then(() => {
|
||||
setSslVerification(!sslVerification);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sslVerification}
|
||||
onChange={handleCheckboxChange}
|
||||
className="mr-3 mousetrap"
|
||||
/>
|
||||
SSL Certificate Verification
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default General;
|
||||
@@ -0,0 +1,36 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.tabs {
|
||||
margin-top: -0.5rem;
|
||||
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.tab-panel {
|
||||
min-height: 300px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,8 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
color: var(--color-text);
|
||||
.collection-options {
|
||||
color: ${(props) => props.theme.text};
|
||||
.rows {
|
||||
svg {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Support = () => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="rows">
|
||||
<div className="mt-2">
|
||||
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end">
|
||||
<IconBook size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Documentation</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
|
||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Report Issues</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://discord.com/invite/KgcZUncpjq" target="_blank" className="flex items-end">
|
||||
<IconBrandDiscord size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Discord</span>
|
||||
</a>
|
||||
</div>
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://twitter.com/use_bruno" target="_blank" className="flex items-end">
|
||||
<IconBrandTwitter size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Twitter</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Support;
|
||||
@@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
color: var(--color-text);
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
64
packages/bruno-app/src/components/Preferences/Theme/index.js
Normal file
64
packages/bruno-app/src/components/Preferences/Theme/index.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const Theme = () => {
|
||||
const { storedTheme, setStoredTheme } = useTheme();
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
theme: storedTheme
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
theme: Yup.string().oneOf(['light', 'dark']).required('theme is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
setStoredTheme(values.theme);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className='bruno-form'>
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="light-theme"
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="theme"
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
formik.handleSubmit()
|
||||
}}
|
||||
value="light"
|
||||
checked={formik.values.theme === 'light'}
|
||||
/>
|
||||
<label htmlFor="light-theme" className="ml-1 cursor-pointer select-none">
|
||||
Light
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="dark-theme"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="theme"
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
formik.handleSubmit()
|
||||
}}
|
||||
value="dark"
|
||||
checked={formik.values.theme === 'dark'}
|
||||
/>
|
||||
<label htmlFor="dark-theme" className="ml-1 cursor-pointer select-none">
|
||||
Dark
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Theme;
|
||||
54
packages/bruno-app/src/components/Preferences/index.js
Normal file
54
packages/bruno-app/src/components/Preferences/index.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import Modal from 'components/Modal/index';
|
||||
import classnames from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
import Support from './Support';
|
||||
import General from './General';
|
||||
import Theme from './Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Preferences = ({ onClose }) => {
|
||||
const [tab, setTab] = useState('general');
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === tab
|
||||
});
|
||||
};
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'general': {
|
||||
return <General />;
|
||||
}
|
||||
|
||||
case 'theme': {
|
||||
return <Theme />;
|
||||
}
|
||||
|
||||
case 'support': {
|
||||
return <Support />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="lg" title="Preferences" handleCancel={onClose} hideFooter={true}>
|
||||
<div className="flex items-center px-2 tabs" role="tablist">
|
||||
<div className={getTabClassname('general')} role="tab" onClick={() => setTab('general')}>
|
||||
General
|
||||
</div>
|
||||
<div className={getTabClassname('theme')} role="tab" onClick={() => setTab('theme')}>
|
||||
Theme
|
||||
</div>
|
||||
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
|
||||
Support
|
||||
</div>
|
||||
</div>
|
||||
<section className="flex flex-grow px-2 mt-4 tab-panel">{getTabPanel(tab)}</section>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Preferences;
|
||||
@@ -27,6 +27,10 @@ const Wrapper = styled.div`
|
||||
&:nth-child(4) {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
select {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ const Collections = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col">
|
||||
<div className="mt-4 flex flex-col overflow-y-auto absolute top-32 bottom-10 left-0 right-0">
|
||||
{collections && collections.length
|
||||
? collections.map((c) => {
|
||||
return (
|
||||
@@ -77,4 +77,3 @@ const Collections = () => {
|
||||
};
|
||||
|
||||
export default Collections;
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
background-color: ${(props) => props.theme.menubar.bg};
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
min-height: 100vh;
|
||||
|
||||
.menu-item {
|
||||
padding: 0.6rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
color: rgba(255, 255, 255);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,60 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
import { toggleLeftMenuBar } from 'providers/ReduxStore/slices/app';
|
||||
import { IconCode, IconFiles, IconMoon, IconChevronsLeft, IconLifebuoy } from '@tabler/icons';
|
||||
|
||||
import Link from 'next/link';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import BrunoSupport from 'components/BrunoSupport';
|
||||
import SwitchTheme from 'components/SwitchTheme';
|
||||
|
||||
const MenuBar = () => {
|
||||
const router = useRouter();
|
||||
const dispatch = useDispatch();
|
||||
const [openTheme, setOpenTheme] = useState(false);
|
||||
const [openBrunoSupport, setOpenBrunoSupport] = useState(false);
|
||||
const isPlatformElectron = isElectron();
|
||||
|
||||
const getClassName = (menu) => {
|
||||
return router.pathname === menu ? 'active menu-item' : 'menu-item';
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full flex flex-col">
|
||||
{openBrunoSupport && <BrunoSupport onClose={() => setOpenBrunoSupport(false)} />}
|
||||
{openTheme && <SwitchTheme onClose={() => setOpenTheme(false)} />}
|
||||
|
||||
<div className="flex flex-col">
|
||||
{/* Todo: Fix this: Clicking on this crashes the app */}
|
||||
{/* <Link href="/">
|
||||
<div className={getClassName('/')}>
|
||||
<IconCode size={28} strokeWidth={1.5} />
|
||||
</div>
|
||||
</Link> */}
|
||||
{/* <div className="menu-item">
|
||||
<IconUsers size={28} strokeWidth={1.5}/>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow justify-end">
|
||||
{/* <Link href="/login">
|
||||
<div className="menu-item">
|
||||
<IconUser size={28} strokeWidth={1.5}/>
|
||||
</div>
|
||||
</Link> */}
|
||||
<div className="menu-item" onClick={() => setOpenBrunoSupport(true)}>
|
||||
<IconLifebuoy size={28} strokeWidth={1.5}/>
|
||||
</div>
|
||||
<div className="menu-item" onClick={() => setOpenTheme(true)}>
|
||||
<IconMoon size={28} strokeWidth={1.5}/>
|
||||
</div>
|
||||
<div className="menu-item" onClick={() => dispatch(toggleLeftMenuBar())}>
|
||||
<IconChevronsLeft size={28} strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuBar;
|
||||
@@ -1,13 +1,13 @@
|
||||
import MenuBar from './MenuBar';
|
||||
import TitleBar from './TitleBar';
|
||||
import Collections from './Collections';
|
||||
import StyledWrapper, { BottomWrapper, VersionNumber } from './StyledWrapper';
|
||||
import GitHubButton from 'react-github-btn'
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import GitHubButton from 'react-github-btn';
|
||||
import Preferences from 'components/Preferences';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconChevronsRight } from '@tabler/icons';
|
||||
import { updateLeftSidebarWidth, updateIsDragging, toggleLeftMenuBar } from 'providers/ReduxStore/slices/app';
|
||||
import { IconSettings } from '@tabler/icons';
|
||||
import { updateLeftSidebarWidth, updateIsDragging } from 'providers/ReduxStore/slices/app';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const MIN_LEFT_SIDEBAR_WIDTH = 222;
|
||||
@@ -15,13 +15,11 @@ const MAX_LEFT_SIDEBAR_WIDTH = 600;
|
||||
|
||||
const Sidebar = () => {
|
||||
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
|
||||
const leftMenuBarOpen = useSelector((state) => state.app.leftMenuBarOpen);
|
||||
const [preferencesOpen, setPreferencesOpen] = useState(false);
|
||||
|
||||
const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);
|
||||
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [dragging, setDragging] = useState(false);
|
||||
@@ -76,27 +74,23 @@ const Sidebar = () => {
|
||||
setAsideWidth(leftSidebarWidth);
|
||||
}, [leftSidebarWidth]);
|
||||
|
||||
const leftMenuBarWidth = leftMenuBarOpen ? 48 : 0;
|
||||
const collectionsWidth = asideWidth - leftMenuBarWidth;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex relative">
|
||||
<StyledWrapper className="flex relative h-screen">
|
||||
<aside>
|
||||
<div className="flex flex-row h-full w-full">
|
||||
{leftMenuBarOpen && <MenuBar />}
|
||||
<div className="flex flex-row h-screen w-full">
|
||||
{preferencesOpen && <Preferences onClose={() => setPreferencesOpen(false)} />}
|
||||
|
||||
<div className="flex flex-col w-full" style={{width: collectionsWidth}}>
|
||||
<div className="flex flex-col w-full" style={{ width: asideWidth }}>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<TitleBar />
|
||||
<Collections />
|
||||
</div>
|
||||
|
||||
<div className="footer flex px-1 py-2 items-center cursor-pointer select-none">
|
||||
<div className="footer flex px-1 py-2 absolute bottom-0 left-0 right-0 items-center cursor-pointer select-none">
|
||||
<div className="flex items-center ml-1 text-xs ">
|
||||
{!leftMenuBarOpen && <IconChevronsRight size={24} strokeWidth={1.5} className="mr-2 hover:text-gray-700" onClick={() => dispatch(toggleLeftMenuBar())} />}
|
||||
{/* <IconLayoutGrid size={20} strokeWidth={1.5} className="mr-2"/> */}
|
||||
<IconSettings size={18} strokeWidth={1.5} className="mr-2 hover:text-gray-700" onClick={() => setPreferencesOpen(true)} />
|
||||
</div>
|
||||
<div className="pl-1" style={{position: 'relative', top: '3px'}}>
|
||||
<div className="pl-1" style={{ position: 'relative', top: '3px' }}>
|
||||
{storedTheme === 'dark' ? (
|
||||
<GitHubButton
|
||||
href="https://github.com/usebruno/bruno"
|
||||
@@ -117,7 +111,7 @@ const Sidebar = () => {
|
||||
</GitHubButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.10.2</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.14.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Modal from 'components/Modal/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const SwitchTheme = ({ onClose }) => {
|
||||
const { storedTheme, setStoredTheme } = useTheme();
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
theme: storedTheme
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
theme: Yup.string().oneOf(['light', 'dark']).required('theme is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
setStoredTheme(values.theme);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={'Switch Theme'} handleCancel={onClose} hideFooter={true}>
|
||||
<div className='bruno-form'>
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="light-theme"
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="theme"
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
formik.handleSubmit()
|
||||
}}
|
||||
value="light"
|
||||
checked={formik.values.theme === 'light'}
|
||||
/>
|
||||
<label htmlFor="light-theme" className="ml-1 cursor-pointer select-none">
|
||||
Light
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="dark-theme"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="theme"
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
formik.handleSubmit()
|
||||
}}
|
||||
value="dark"
|
||||
checked={formik.values.theme === 'dark'}
|
||||
/>
|
||||
<label htmlFor="dark-theme" className="ml-1 cursor-pointer select-none">
|
||||
Dark
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SwitchTheme;
|
||||
@@ -26,7 +26,7 @@ const VariablesTable = ({ variables, collectionVariables }) => {
|
||||
<div className='variable-value pl-2 whitespace-normal text-left flex-grow'>{variable.value}</div>
|
||||
</div>
|
||||
);
|
||||
}) : null}
|
||||
}) : <small>No env variables found</small>}
|
||||
|
||||
<div className='mt-2 font-medium'>Collection Variables</div>
|
||||
{(collectionVars && collectionVars.length) ? collectionVars.map((variable) => {
|
||||
@@ -36,7 +36,7 @@ const VariablesTable = ({ variables, collectionVariables }) => {
|
||||
<div className='variable-value pl-2 whitespace-normal text-left flex-grow'>{variable.value}</div>
|
||||
</div>
|
||||
);
|
||||
}) : null}
|
||||
}) : <small>No collection variables found</small>}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ const VariablesView = ({collection}) => {
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
const variables = get(environment, 'variables', []);
|
||||
const enabledVariables = filter(variables, (variable) => variable.enabled);
|
||||
const showVariablesTable = enabledVariables.length > 0 || (collection.collectionVariables && Object.keys(collection.collectionVariables).length > 0);
|
||||
|
||||
return (
|
||||
<StyledWrapper
|
||||
@@ -34,7 +35,12 @@ const VariablesView = ({collection}) => {
|
||||
handleClose={() => setPopOverOpen(false)}
|
||||
>
|
||||
<div className="px-2 py-1">
|
||||
{(enabledVariables && enabledVariables.length) ? <VariablesTable variables={enabledVariables} collectionVariables={collection.collectionVariables}/> : 'No variables found'}
|
||||
{showVariablesTable ? (
|
||||
<VariablesTable
|
||||
variables={enabledVariables}
|
||||
collectionVariables={collection.collectionVariables}
|
||||
/>
|
||||
) : 'No variables found'}
|
||||
</div>
|
||||
</PopOver>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
max-height: 100vh;
|
||||
|
||||
&.is-dragging {
|
||||
cursor: col-resize !important;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Provider } from 'react-redux';
|
||||
import { AppProvider } from 'providers/App';
|
||||
import { ToastProvider } from 'providers/Toaster';
|
||||
import { HotkeysProvider } from 'providers/Hotkeys';
|
||||
import { PreferencesProvider } from 'providers/Preferences';
|
||||
|
||||
import ReduxStore from 'providers/ReduxStore';
|
||||
import ThemeProvider from 'providers/Theme/index';
|
||||
@@ -46,9 +47,11 @@ function MyApp({ Component, pageProps }) {
|
||||
<ThemeProvider>
|
||||
<ToastProvider>
|
||||
<AppProvider>
|
||||
<HotkeysProvider>
|
||||
<Component {...pageProps} />
|
||||
</HotkeysProvider>
|
||||
<PreferencesProvider>
|
||||
<HotkeysProvider>
|
||||
<Component {...pageProps} />
|
||||
</HotkeysProvider>
|
||||
</PreferencesProvider>
|
||||
</AppProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -7,12 +7,9 @@ import {
|
||||
collectionUnlinkFileEvent,
|
||||
collectionUnlinkDirectoryEvent,
|
||||
collectionUnlinkEnvFileEvent,
|
||||
requestSentEvent,
|
||||
requestQueuedEvent,
|
||||
testResultsEvent,
|
||||
assertionResultsEvent,
|
||||
scriptEnvironmentUpdateEvent,
|
||||
collectionRenamedEvent,
|
||||
runRequestEvent,
|
||||
runFolderEvent
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -96,26 +93,10 @@ const useCollectionTreeSync = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const _httpRequestSent = (val) => {
|
||||
dispatch(requestSentEvent(val));
|
||||
};
|
||||
|
||||
const _scriptEnvironmentUpdate = (val) => {
|
||||
dispatch(scriptEnvironmentUpdateEvent(val));
|
||||
};
|
||||
|
||||
const _httpRequestQueued = (val) => {
|
||||
dispatch(requestQueuedEvent(val));
|
||||
};
|
||||
|
||||
const _testResults = (val) => {
|
||||
dispatch(testResultsEvent(val));
|
||||
};
|
||||
|
||||
const _assertionResults = (val) => {
|
||||
dispatch(assertionResultsEvent(val));
|
||||
};
|
||||
|
||||
const _collectionRenamed = (val) => {
|
||||
dispatch(collectionRenamedEvent(val));
|
||||
};
|
||||
@@ -124,19 +105,23 @@ const useCollectionTreeSync = () => {
|
||||
dispatch(runFolderEvent(val));
|
||||
};
|
||||
|
||||
const _runRequestEvent = (val) => {
|
||||
dispatch(runRequestEvent(val));
|
||||
};
|
||||
|
||||
ipcRenderer.invoke('renderer:ready');
|
||||
|
||||
const removeListener1 = ipcRenderer.on('main:collection-opened', _openCollection);
|
||||
const removeListener2 = ipcRenderer.on('main:collection-tree-updated', _collectionTreeUpdated);
|
||||
const removeListener3 = ipcRenderer.on('main:collection-already-opened', _collectionAlreadyOpened);
|
||||
const removeListener4 = ipcRenderer.on('main:display-error', _displayError);
|
||||
const removeListener5 = ipcRenderer.on('main:http-request-sent', _httpRequestSent);
|
||||
const removeListener6 = ipcRenderer.on('main:script-environment-update', _scriptEnvironmentUpdate);
|
||||
const removeListener7 = ipcRenderer.on('main:http-request-queued', _httpRequestQueued);
|
||||
const removeListener8 = ipcRenderer.on('main:test-results', _testResults);
|
||||
const removeListener9 = ipcRenderer.on('main:assertion-results', _assertionResults);
|
||||
const removeListener10 = ipcRenderer.on('main:collection-renamed', _collectionRenamed);
|
||||
const removeListener11 = ipcRenderer.on('main:run-folder-event', _runFolderEvent);
|
||||
const removeListener5 = ipcRenderer.on('main:script-environment-update', _scriptEnvironmentUpdate);
|
||||
const removeListener6 = ipcRenderer.on('main:collection-renamed', _collectionRenamed);
|
||||
const removeListener7 = ipcRenderer.on('main:run-folder-event', _runFolderEvent);
|
||||
const removeListener8 = ipcRenderer.on('main:run-request-event', _runRequestEvent);
|
||||
const removeListener9 = ipcRenderer.on('main:console-log', (val) => {
|
||||
console[val.type](...val.args);
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeListener1();
|
||||
@@ -148,8 +133,6 @@ const useCollectionTreeSync = () => {
|
||||
removeListener7();
|
||||
removeListener8();
|
||||
removeListener9();
|
||||
removeListener10();
|
||||
removeListener11();
|
||||
};
|
||||
}, [isElectron]);
|
||||
};
|
||||
|
||||
80
packages/bruno-app/src/providers/Preferences/index.js
Normal file
80
packages/bruno-app/src/providers/Preferences/index.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Preferences Provider
|
||||
*
|
||||
* This provider is responsible for managing the user's preferences in the app.
|
||||
* The preferences are stored in the browser local storage.
|
||||
*
|
||||
* On start, an IPC event is published to the main process to set the preferences in the electron process.
|
||||
*/
|
||||
|
||||
import { useEffect, createContext, useContext } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import useLocalStorage from 'hooks/useLocalStorage/index';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const defaultPreferences = {
|
||||
request: {
|
||||
sslVerification: true
|
||||
}
|
||||
};
|
||||
|
||||
const preferencesSchema = Yup.object().shape({
|
||||
request: Yup.object().shape({
|
||||
sslVerification: Yup.boolean()
|
||||
})
|
||||
});
|
||||
|
||||
export const PreferencesContext = createContext();
|
||||
export const PreferencesProvider = (props) => {
|
||||
const [preferences, setPreferences] = useLocalStorage('bruno.preferences', defaultPreferences);
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
useEffect(() => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:set-preferences', preferences)
|
||||
.catch(err => {
|
||||
toast.error(err.message || 'Preferences sync error');
|
||||
});
|
||||
}, [preferences, toast]);
|
||||
|
||||
const validatedSetPreferences = (newPreferences) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
preferencesSchema.validate(newPreferences, { abortEarly: true })
|
||||
.then(validatedPreferences => {
|
||||
setPreferences(validatedPreferences);
|
||||
resolve(validatedPreferences);
|
||||
})
|
||||
.catch(error => {
|
||||
let errMsg = error.message || 'Preferences validation error';
|
||||
toast.error(errMsg);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// todo: setPreferences must validate the preferences object against a schema
|
||||
const value = {
|
||||
preferences,
|
||||
setPreferences: validatedSetPreferences
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesContext.Provider value={value}>
|
||||
<>
|
||||
{props.children}
|
||||
</>
|
||||
</PreferencesContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const usePreferences = () => {
|
||||
const context = useContext(PreferencesContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error(`usePreferences must be used within a PreferencesProvider`);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export default PreferencesProvider;
|
||||
@@ -4,7 +4,6 @@ const initialState = {
|
||||
isDragging: false,
|
||||
idbConnectionReady: false,
|
||||
leftSidebarWidth: 222,
|
||||
leftMenuBarOpen: false,
|
||||
screenWidth: 500,
|
||||
showHomePage: false
|
||||
};
|
||||
@@ -16,14 +15,6 @@ export const appSlice = createSlice({
|
||||
idbConnectionReady: (state) => {
|
||||
state.idbConnectionReady = true;
|
||||
},
|
||||
toggleLeftMenuBar: (state) => {
|
||||
state.leftMenuBarOpen = !state.leftMenuBarOpen;
|
||||
if(state.leftMenuBarOpen) {
|
||||
state.leftSidebarWidth += 48;
|
||||
} else {
|
||||
state.leftSidebarWidth -= 48;
|
||||
}
|
||||
},
|
||||
refreshScreenWidth: (state) => {
|
||||
state.screenWidth = window.innerWidth;
|
||||
},
|
||||
@@ -42,6 +33,6 @@ export const appSlice = createSlice({
|
||||
}
|
||||
});
|
||||
|
||||
export const { idbConnectionReady, toggleLeftMenuBar, refreshScreenWidth, updateLeftSidebarWidth, updateIsDragging, showHomePage, hideHomePage } = appSlice.actions;
|
||||
export const { idbConnectionReady, refreshScreenWidth, updateLeftSidebarWidth, updateIsDragging, showHomePage, hideHomePage } = appSlice.actions;
|
||||
|
||||
export default appSlice.reducer;
|
||||
|
||||
@@ -122,6 +122,12 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
response: null
|
||||
})
|
||||
);
|
||||
|
||||
if(err && err.message === "Error invoking remote method 'send-http-request': Error: Request cancelled") {
|
||||
console.log('>> request cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('>> sending request failed');
|
||||
console.log(err);
|
||||
toast.error(err ? err.message : 'Something went wrong!');
|
||||
|
||||
@@ -156,32 +156,6 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
requestSentEvent: (state, action) => {
|
||||
const { itemUid, collectionUid, cancelTokenUid, requestSent } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (item) {
|
||||
item.requestSent = requestSent
|
||||
item.response = item.response || {};
|
||||
item.requestState = 'sending';
|
||||
item.cancelTokenUid = cancelTokenUid;
|
||||
}
|
||||
}
|
||||
},
|
||||
requestQueuedEvent: (state, action) => {
|
||||
const { itemUid, collectionUid, cancelTokenUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (item) {
|
||||
item.requestState = 'queued';
|
||||
item.cancelTokenUid = cancelTokenUid;
|
||||
}
|
||||
}
|
||||
},
|
||||
scriptEnvironmentUpdateEvent: (state, action) => {
|
||||
const { collectionUid, envVariables, collectionVariables } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -1010,31 +984,6 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
testResultsEvent: (state, action) => {
|
||||
const { itemUid, collectionUid, results } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
|
||||
if (item) {
|
||||
item.testResults = results;
|
||||
}
|
||||
}
|
||||
},
|
||||
assertionResultsEvent: (state, action) => {
|
||||
const { itemUid, collectionUid, results } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
console.log(results);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
|
||||
if (item) {
|
||||
item.assertionResults = results;
|
||||
}
|
||||
}
|
||||
},
|
||||
collectionRenamedEvent: (state, action) => {
|
||||
const { collectionPathname, newName } = action.payload;
|
||||
const collection = findCollectionByPathname(state.collections, collectionPathname);
|
||||
@@ -1075,6 +1024,45 @@ export const collectionsSlice = createSlice({
|
||||
collection.runnerResult = null;
|
||||
}
|
||||
},
|
||||
runRequestEvent: (state, action) => {
|
||||
const { itemUid, collectionUid, type, requestUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (item) {
|
||||
if(type === 'request-queued') {
|
||||
const { cancelTokenUid } = action.payload;
|
||||
item.requestUid = requestUid;
|
||||
item.requestState = 'queued';
|
||||
item.response = null;
|
||||
item.cancelTokenUid = cancelTokenUid;
|
||||
}
|
||||
|
||||
if(type === 'request-sent') {
|
||||
const { cancelTokenUid, requestSent } = action.payload;
|
||||
item.requestSent = requestSent;
|
||||
|
||||
// sometimes the response is received before the request-sent event arrives
|
||||
if(item.requestUid === requestUid && item.requestState === 'queued') {
|
||||
item.requestUid = requestUid;
|
||||
item.requestState = 'sending';
|
||||
item.cancelTokenUid = cancelTokenUid;
|
||||
}
|
||||
}
|
||||
|
||||
if(type === 'assertion-results') {
|
||||
const { results } = action.payload;
|
||||
item.assertionResults = results;
|
||||
}
|
||||
|
||||
if(type === 'test-results') {
|
||||
const { results } = action.payload;
|
||||
item.testResults = results;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
runFolderEvent: (state, action) => {
|
||||
const { collectionUid, folderUid, itemUid, type, isRecursive, error } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -1162,8 +1150,6 @@ export const {
|
||||
deleteItem,
|
||||
renameItem,
|
||||
cloneItem,
|
||||
requestSentEvent,
|
||||
requestQueuedEvent,
|
||||
scriptEnvironmentUpdateEvent,
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
@@ -1204,13 +1190,12 @@ export const {
|
||||
collectionUnlinkFileEvent,
|
||||
collectionUnlinkDirectoryEvent,
|
||||
collectionAddEnvFileEvent,
|
||||
testResultsEvent,
|
||||
assertionResultsEvent,
|
||||
collectionRenamedEvent,
|
||||
toggleRunnerView,
|
||||
showRunnerView,
|
||||
hideRunnerView,
|
||||
resetRunResults,
|
||||
runRequestEvent,
|
||||
runFolderEvent,
|
||||
closeCollectionRunner
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import themes from 'themes/index';
|
||||
import useLocalStorage from 'src/hooks/useLocalStorage/index';
|
||||
import useLocalStorage from 'hooks/useLocalStorage/index';
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
import { ThemeProvider as SCThemeProvider } from 'styled-components';
|
||||
|
||||
@@ -49,3 +49,15 @@ export const safeStringifyJSON = (obj, indent=false) => {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any characters that are not alphanumeric, spaces, hyphens, or underscores
|
||||
export const normalizeFileName = (name) => {
|
||||
if (!name) {
|
||||
return name;
|
||||
}
|
||||
|
||||
const validChars = /[^\w\s-]/g;
|
||||
const formattedName = name.replace(validChars, '-');
|
||||
|
||||
return formattedName;
|
||||
}
|
||||
|
||||
19
packages/bruno-app/src/utils/common/index.spec.js
Normal file
19
packages/bruno-app/src/utils/common/index.spec.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const { describe, it, expect } = require("@jest/globals");
|
||||
|
||||
import { normalizeFileName } from './index';
|
||||
|
||||
describe("common utils", () => {
|
||||
describe("normalizeFileName", () => {
|
||||
it("should remove special characters", () => {
|
||||
expect(normalizeFileName("hello world")).toBe("hello world");
|
||||
expect(normalizeFileName("hello-world")).toBe("hello-world");
|
||||
expect(normalizeFileName("hello_world")).toBe("hello_world");
|
||||
expect(normalizeFileName("hello_world-")).toBe("hello_world-");
|
||||
expect(normalizeFileName("hello_world-123")).toBe("hello_world-123");
|
||||
expect(normalizeFileName("hello_world-123!@#$%^&*()")).toBe("hello_world-123----------");
|
||||
expect(normalizeFileName("hello_world?")).toBe("hello_world-");
|
||||
expect(normalizeFileName("foo/bar/")).toBe("foo-bar-");
|
||||
expect(normalizeFileName("foo\\bar\\")).toBe("foo-bar-");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { uuid } from 'utils/common';
|
||||
import { uuid, normalizeFileName } from 'utils/common';
|
||||
import { isItemARequest } from 'utils/collections';
|
||||
import { collectionSchema } from '@usebruno/schema';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
@@ -63,6 +63,8 @@ export const updateUidsInCollection = (_collection) => {
|
||||
export const transformItemsInCollection = (collection) => {
|
||||
const transformItems = (items = []) => {
|
||||
each(items, (item) => {
|
||||
item.name = normalizeFileName(item.name);
|
||||
|
||||
if (['http', 'graphql'].includes(item.type)) {
|
||||
item.type = `${item.type}-request`;
|
||||
if(item.request.query) {
|
||||
|
||||
BIN
packages/bruno-cli/assets/images/cli-demo.png
Normal file
BIN
packages/bruno-cli/assets/images/cli-demo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
9
packages/bruno-cli/changelog.md
Normal file
9
packages/bruno-cli/changelog.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## 0.7.1
|
||||
|
||||
* `--cacert` flag to support custom CA certificates
|
||||
|
||||
## 0.7.0
|
||||
|
||||
* `--insecure` flag to disable SSL verification
|
||||
22
packages/bruno-cli/license.md
Normal file
22
packages/bruno-cli/license.md
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Anoop M D, Anusree P S and Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,18 +1,27 @@
|
||||
{
|
||||
"name": "@usebruno/cli",
|
||||
"version": "0.3.0",
|
||||
"version": "0.7.1",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"bru": "./bin/bru.js"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/usebruno/bruno/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/usebruno/bruno.git"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"bin",
|
||||
"readme.md",
|
||||
"changelog.md",
|
||||
"package.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"@usebruno/js": "0.2.0",
|
||||
"@usebruno/lang": "0.2.2",
|
||||
"@usebruno/js": "0.4.0",
|
||||
"@usebruno/lang": "0.3.0",
|
||||
"axios": "^1.3.2",
|
||||
"chai": "^4.3.7",
|
||||
"chalk": "^3.0.0",
|
||||
|
||||
@@ -1,8 +1,46 @@
|
||||
# bruno-cli
|
||||
|
||||
Bru CLI
|
||||
With Bruno CLI, you can now run your API collections with ease using simple command line commands.
|
||||
|
||||
### Publish to Npm Registry
|
||||
This makes it easier to test your APIs in different environments, automate your testing process, and integrate your API tests with your continuous integration and deployment workflows.
|
||||
|
||||
## Installation
|
||||
To install the Bruno CLI, use the node package manager of your choice, such as NPM:
|
||||
```bash
|
||||
npm publish --access=public
|
||||
```
|
||||
npm install -g @usebruno/cli
|
||||
```
|
||||
|
||||
## Getting started
|
||||
Navigate to the directory where your API collection resides, and then run:
|
||||
```bash
|
||||
bru run
|
||||
```
|
||||
This command will run all the requests in your collection. You can also run a single request by specifying its filename:
|
||||
|
||||
```bash
|
||||
bru run request.bru
|
||||
```
|
||||
|
||||
Or run all requests in a collection's subfolder:
|
||||
```bash
|
||||
bru run folder
|
||||
```
|
||||
|
||||
If you need to use an environment, you can specify it with the --env option:
|
||||
```bash
|
||||
bru run folder --env Local
|
||||
```
|
||||
|
||||
## Demo
|
||||

|
||||
|
||||
## Support
|
||||
If you encounter any issues or have any feedback or suggestions, please raise them on our [GitHub repository](https://github.com/usebruno/bruno)
|
||||
|
||||
Thank you for using Bruno CLI!
|
||||
|
||||
## Changelog
|
||||
See [here](packages/bruno-cli/changelog.md)
|
||||
|
||||
## License
|
||||
[MIT](license.md)
|
||||
@@ -5,7 +5,7 @@ const { exists, isFile, isDirectory, getSubDirectories } = require('../utils/fil
|
||||
const { runSingleRequest } = require('../runner/run-single-request');
|
||||
const { bruToEnvJson, getEnvVars } = require('../utils/bru');
|
||||
const { rpad } = require('../utils/common');
|
||||
const { bruToJson } = require('../utils/bru');
|
||||
const { bruToJson, getOptions } = require('../utils/bru');
|
||||
|
||||
const command = 'run [filename]';
|
||||
const desc = 'Run a request';
|
||||
@@ -35,6 +35,15 @@ const printRunSummary = (assertionResults, testResults) => {
|
||||
|
||||
console.log("\n" + chalk.bold(assertSummary));
|
||||
console.log(chalk.bold(testSummary));
|
||||
|
||||
return {
|
||||
totalAssertions,
|
||||
passedAssertions,
|
||||
failedAssertions,
|
||||
totalTests,
|
||||
passedTests,
|
||||
failedTests
|
||||
}
|
||||
};
|
||||
|
||||
const getBruFilesRecursively = (dir) => {
|
||||
@@ -45,6 +54,10 @@ const getBruFilesRecursively = (dir) => {
|
||||
|
||||
const traverse = (currentPath) => {
|
||||
const filesInCurrentDir = fs.readdirSync(currentPath);
|
||||
|
||||
if (currentPath.includes('node_modules')) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of filesInCurrentDir) {
|
||||
const filePath = path.join(currentPath, file);
|
||||
@@ -100,10 +113,18 @@ const builder = async (yargs) => {
|
||||
type: 'boolean',
|
||||
default: false
|
||||
})
|
||||
.option('cacert', {
|
||||
type: 'string',
|
||||
description: 'CA certificate to verify peer against'
|
||||
})
|
||||
.option('env', {
|
||||
describe: 'Environment variables',
|
||||
type: 'string',
|
||||
})
|
||||
.option('insecure', {
|
||||
type: 'boolean',
|
||||
description: 'Allow insecure server connections'
|
||||
})
|
||||
.example('$0 run request.bru', 'Run a request')
|
||||
.example('$0 run request.bru --env local', 'Run a request with the environment set to local')
|
||||
.example('$0 run folder', 'Run all requests in a folder')
|
||||
@@ -114,7 +135,9 @@ const handler = async function (argv) {
|
||||
try {
|
||||
let {
|
||||
filename,
|
||||
cacert,
|
||||
env,
|
||||
insecure,
|
||||
r: recursive
|
||||
} = argv;
|
||||
const collectionPath = process.cwd();
|
||||
@@ -157,6 +180,25 @@ const handler = async function (argv) {
|
||||
envVars = getEnvVars(envJson);
|
||||
}
|
||||
|
||||
const options = getOptions();
|
||||
if(insecure) {
|
||||
options['insecure'] = true
|
||||
}
|
||||
if(cacert && cacert.length) {
|
||||
if(insecure) {
|
||||
console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`));
|
||||
}
|
||||
else {
|
||||
const pathExists = await exists(cacert);
|
||||
if(pathExists) {
|
||||
options['cacert'] = cacert
|
||||
}
|
||||
else {
|
||||
console.error(chalk.red(`Cacert File ${cacert} does not exist`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const _isFile = await isFile(filename);
|
||||
if(_isFile) {
|
||||
console.log(chalk.yellow('Running Request \n'));
|
||||
@@ -170,8 +212,14 @@ const handler = async function (argv) {
|
||||
testResults
|
||||
} = result;
|
||||
|
||||
printRunSummary(assertionResults, testResults);
|
||||
const summary = printRunSummary(assertionResults, testResults);
|
||||
console.log(chalk.dim(chalk.grey('Done.')));
|
||||
|
||||
if(summary.failedAssertions > 0 || summary.failedTests > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,12 +274,17 @@ const handler = async function (argv) {
|
||||
}
|
||||
}
|
||||
|
||||
printRunSummary(assertionResults, testResults);
|
||||
const summary = printRunSummary(assertionResults, testResults);
|
||||
console.log(chalk.dim(chalk.grey('Ran all requests.')));
|
||||
|
||||
if(summary.failedAssertions > 0 || summary.failedTests > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Something went wrong");
|
||||
console.error(chalk.red(err.message));
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -42,6 +42,17 @@ const interpolateVars = (request, envVars = {}, collectionVariables ={}) => {
|
||||
request.data = interpolate(request.data);
|
||||
}
|
||||
}
|
||||
} else if(request.headers["content-type"] === "application/x-www-form-urlencoded") {
|
||||
if(typeof request.data === "object") {
|
||||
try {
|
||||
let parsed = JSON.stringify(request.data);
|
||||
parsed = interpolate(parsed);
|
||||
request.data = JSON.parse(parsed);
|
||||
} catch (err) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
request.data = interpolate(request.data);
|
||||
}
|
||||
|
||||
each(request.params, (param) => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const { get, each, filter } = require('lodash');
|
||||
const qs = require('qs');
|
||||
|
||||
const prepareRequest = (request) => {
|
||||
const headers = {};
|
||||
@@ -41,7 +40,7 @@ const prepareRequest = (request) => {
|
||||
const params = {};
|
||||
const enabledParams = filter(request.body.formUrlEncoded, (p) => p.enabled);
|
||||
each(enabledParams, (p) => (params[p.name] = p.value));
|
||||
axiosRequest.data = qs.stringify(params);
|
||||
axiosRequest.data = params;
|
||||
}
|
||||
|
||||
if (request.body.mode === 'multipartForm') {
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
const qs = require('qs');
|
||||
const chalk = require('chalk');
|
||||
const fs = require('fs');
|
||||
const { forOwn, each, extend, get } = require('lodash');
|
||||
const FormData = require('form-data');
|
||||
const axios = require('axios');
|
||||
const https = require('https');
|
||||
const prepareRequest = require('./prepare-request');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime } = require('@usebruno/js');
|
||||
const { stripExtension } = require('../utils/filesystem');
|
||||
const { getOptions } = require('../utils/bru');
|
||||
|
||||
const runSingleRequest = async function (filename, bruJson, collectionPath, collectionVariables, envVariables) {
|
||||
let request;
|
||||
|
||||
try {
|
||||
const request = prepareRequest(bruJson.request);
|
||||
request = prepareRequest(bruJson.request);
|
||||
|
||||
// make axios work in node using form data
|
||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||
@@ -33,12 +39,42 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
const requestScriptFile = get(bruJson, 'request.script.req');
|
||||
if(requestScriptFile && requestScriptFile.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
scriptRuntime.runRequestScript(requestScriptFile, request, envVariables, collectionVariables, collectionPath);
|
||||
await scriptRuntime.runRequestScript(requestScriptFile, request, envVariables, collectionVariables, collectionPath);
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
interpolateVars(request, envVariables, collectionVariables);
|
||||
|
||||
const options = getOptions();
|
||||
const insecure = get(options, 'insecure', false);
|
||||
const httpsAgentRequestFields = {};
|
||||
if(insecure) {
|
||||
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
||||
}
|
||||
else {
|
||||
const cacertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS];
|
||||
const cacert = cacertArray.find(el => el);
|
||||
if(cacert && cacert.length > 1) {
|
||||
try {
|
||||
caCrt = fs.readFileSync(cacert);
|
||||
httpsAgentRequestFields['ca'] = caCrt;
|
||||
} catch(err) {
|
||||
console.log('Error reading CA cert file:' + cacert, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(Object.keys(httpsAgentRequestFields).length > 0) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
...httpsAgentRequestFields
|
||||
});
|
||||
}
|
||||
|
||||
// stringify the request url encoded params
|
||||
if(request.headers['content-type'] === 'application/x-www-form-urlencoded') {
|
||||
request.data = qs.stringify(request.data);
|
||||
}
|
||||
|
||||
// run request
|
||||
const response = await axios(request);
|
||||
|
||||
@@ -55,7 +91,7 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
const responseScriptFile = get(bruJson, 'request.script.res');
|
||||
if(responseScriptFile && responseScriptFile.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
scriptRuntime.runResponseScript(responseScriptFile, request, response, envVariables, collectionVariables, collectionPath);
|
||||
await scriptRuntime.runResponseScript(responseScriptFile, request, response, envVariables, collectionVariables, collectionPath);
|
||||
}
|
||||
|
||||
// run assertions
|
||||
@@ -99,7 +135,61 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
testResults
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
|
||||
if(err && err.response) {
|
||||
console.log(chalk.green(stripExtension(filename)) + chalk.dim(` (${err.response.status} ${err.response.statusText})`));
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(bruJson, 'request.vars.res');
|
||||
if(postResponseVars && postResponseVars.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
varsRuntime.runPostResponseVars(postResponseVars, request, err.response, envVariables, collectionVariables, collectionPath);
|
||||
}
|
||||
|
||||
// run post response script
|
||||
const responseScriptFile = get(bruJson, 'request.script.res');
|
||||
if(responseScriptFile && responseScriptFile.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
await scriptRuntime.runResponseScript(responseScriptFile, request, err.response, envVariables, collectionVariables, collectionPath);
|
||||
}
|
||||
|
||||
// run assertions
|
||||
let assertionResults = [];
|
||||
const assertions = get(bruJson, 'request.assertions');
|
||||
if(assertions && assertions.length) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
assertionResults = assertRuntime.runAssertions(assertions, request, err.response, envVariables, collectionVariables, collectionPath);
|
||||
|
||||
each(assertionResults, (r) => {
|
||||
if(r.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
|
||||
console.log(chalk.red(` ${r.error}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// run tests
|
||||
let testResults = [];
|
||||
const testFile = get(bruJson, 'request.tests');
|
||||
if(testFile && testFile.length) {
|
||||
const testRuntime = new TestRuntime();
|
||||
const result = testRuntime.runTests(testFile, request, err.response, envVariables, collectionVariables, collectionPath);
|
||||
testResults = get(result, 'results', []);
|
||||
}
|
||||
|
||||
if(testResults && testResults.length) {
|
||||
each(testResults, (testResult) => {
|
||||
if(testResult.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(testResult.description));
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -80,8 +80,14 @@ const getEnvVars = (environment = {}) => {
|
||||
return envVars;
|
||||
};
|
||||
|
||||
const options = {};
|
||||
const getOptions = () => {
|
||||
return options;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
bruToJson,
|
||||
bruToEnvJson,
|
||||
getEnvVars
|
||||
getEnvVars,
|
||||
getOptions
|
||||
};
|
||||
|
||||
11
packages/bruno-docs/package.json
Normal file
11
packages/bruno-docs/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@usebruno/docs",
|
||||
"version": "0.0.1",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
"package.json"
|
||||
],
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
||||
5
packages/bruno-docs/readme.md
Normal file
5
packages/bruno-docs/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# bruno-docs
|
||||
|
||||
This is a wip.
|
||||
|
||||
We have a request to generate docs in a html file that can be hosted on a server so that the visitor can view the API and make requests without downloading/installing anything.
|
||||
56
packages/bruno-electron/electron-builder-config.js
Normal file
56
packages/bruno-electron/electron-builder-config.js
Normal file
@@ -0,0 +1,56 @@
|
||||
require('dotenv').config({ path: process.env.DOTENV_PATH });
|
||||
|
||||
const config = {
|
||||
"appId": "com.usebruno.app",
|
||||
"productName": "Bruno",
|
||||
"electronVersion": "21.1.1",
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"output": "out"
|
||||
},
|
||||
"files": [
|
||||
"**/*"
|
||||
],
|
||||
"afterSign": "notarize.js",
|
||||
"mac": {
|
||||
"artifactName": "${name}_${version}_${arch}_${os}.${ext}",
|
||||
"category": "public.app-category.developer-tools",
|
||||
"target": [
|
||||
{
|
||||
"target": "dmg",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "resources/icons/mac/icon.icns",
|
||||
"hardenedRuntime": true,
|
||||
"identity": "Anoop MD (W7LPPWA48L)",
|
||||
"entitlements": "resources/entitlements.mac.plist",
|
||||
"entitlementsInherit": "resources/entitlements.mac.plist"
|
||||
},
|
||||
"linux": {
|
||||
"artifactName": "${name}_${version}_${arch}_linux.${ext}",
|
||||
"icon": "resources/icons/png",
|
||||
"target": [
|
||||
"AppImage",
|
||||
"deb"
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"artifactName": "${name}_${version}_${arch}_win.${ext}",
|
||||
"icon": "resources/icons/png",
|
||||
"certificateFile": `${process.env.WIN_CERT_FILEPATH}`,
|
||||
"certificatePassword": `${process.env.WIN_CERT_PASSWORD}`,
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -1,37 +0,0 @@
|
||||
appId: com.usebruno.app
|
||||
productName: Bruno
|
||||
electronVersion: 21.1.1
|
||||
directories:
|
||||
buildResources: resources
|
||||
output: out
|
||||
files:
|
||||
- "**/*"
|
||||
afterSign: notarize.js
|
||||
mac:
|
||||
artifactName: ${name}_${version}_${arch}_${os}.${ext}
|
||||
category: public.app-category.developer-tools
|
||||
target:
|
||||
- target: dmg
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: zip
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
icon: resources/icons/mac/icon.icns
|
||||
hardenedRuntime: true
|
||||
identity: "Anoop MD (W7LPPWA48L)"
|
||||
entitlements: resources/entitlements.mac.plist
|
||||
entitlementsInherit: resources/entitlements.mac.plist
|
||||
linux:
|
||||
artifactName: ${name}_${version}_${arch}_linux.${ext}
|
||||
icon: resources/icons/png
|
||||
target:
|
||||
- AppImage
|
||||
- deb
|
||||
win:
|
||||
artifactName: ${name}_${version}_${arch}_win.${ext}
|
||||
icon: resources/icons/png
|
||||
certificateFile: sectigo.pfx
|
||||
certificatePassword: "secret"
|
||||
@@ -1,4 +1,4 @@
|
||||
require('dotenv').config();
|
||||
require('dotenv').config({ path: process.env.DOTENV_PATH });
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const electron_notarize = require('electron-notarize');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.10.2",
|
||||
"version": "v0.14.0",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
@@ -9,12 +9,12 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"dev": "electron .",
|
||||
"dist": "electron-builder --win --linux --mac",
|
||||
"dist": "electron-builder --mac --config electron-builder-config.js",
|
||||
"pack": "electron-builder --dir"
|
||||
},
|
||||
"dependencies": {
|
||||
"@usebruno/js": "0.2.0",
|
||||
"@usebruno/lang": "0.2.2",
|
||||
"@usebruno/js": "0.3.0",
|
||||
"@usebruno/lang": "0.3.0",
|
||||
"@usebruno/schema": "0.3.1",
|
||||
"axios": "^0.26.0",
|
||||
"chai": "^4.3.7",
|
||||
|
||||
@@ -8,6 +8,7 @@ class LastOpenedCollections {
|
||||
name: 'preferences',
|
||||
clearInvalidConfig: true
|
||||
});
|
||||
console.log(`Preferences file is located at: ${this.store.path}`);
|
||||
}
|
||||
|
||||
getAll() {
|
||||
|
||||
26
packages/bruno-electron/src/app/preferences.js
Normal file
26
packages/bruno-electron/src/app/preferences.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* The preferences are stored in the browser local storage.
|
||||
* When the app is started, an IPC message is published from the renderer process to set the preferences.
|
||||
* The electron process uses this module to get the preferences.
|
||||
*
|
||||
* {
|
||||
* request: {
|
||||
* sslVerification: boolean
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
let preferences = {};
|
||||
|
||||
const getPreferences = () => {
|
||||
return preferences;
|
||||
}
|
||||
|
||||
const setPreferences = (newPreferences) => {
|
||||
preferences = newPreferences;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPreferences,
|
||||
setPreferences
|
||||
};
|
||||
@@ -21,6 +21,7 @@ const { stringifyJson } = require('../utils/common');
|
||||
const { openCollectionDialog, openCollection } = require('../app/collections');
|
||||
const { generateUidBasedOnHash } = require('../utils/common');
|
||||
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
|
||||
const { setPreferences } = require("../app/preferences");
|
||||
|
||||
const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
// browse directory
|
||||
@@ -431,6 +432,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:set-preferences', async (event, preferences) => {
|
||||
setPreferences(preferences);
|
||||
});
|
||||
};
|
||||
|
||||
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const qs = require('qs');
|
||||
const https = require('https');
|
||||
const axios = require('axios');
|
||||
const Mustache = require('mustache');
|
||||
const FormData = require('form-data');
|
||||
@@ -10,6 +12,7 @@ const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../util
|
||||
const { uuid } = require('../../utils/common');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper');
|
||||
const { getPreferences } = require('../../app/preferences');
|
||||
|
||||
// override the default escape function to prevent escaping
|
||||
Mustache.escape = function (value) {
|
||||
@@ -35,7 +38,9 @@ const safeParseJSON = (data) => {
|
||||
const getEnvVars = (environment = {}) => {
|
||||
const variables = environment.variables;
|
||||
if (!variables || !variables.length) {
|
||||
return {};
|
||||
return {
|
||||
__name__: environment.name
|
||||
};
|
||||
}
|
||||
|
||||
const envVars = {};
|
||||
@@ -45,7 +50,10 @@ const getEnvVars = (environment = {}) => {
|
||||
}
|
||||
});
|
||||
|
||||
return envVars;
|
||||
return {
|
||||
...envVars,
|
||||
__name__: environment.name
|
||||
}
|
||||
};
|
||||
|
||||
const getSize = (data) => {
|
||||
@@ -64,21 +72,34 @@ const getSize = (data) => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
const registerNetworkIpc = (mainWindow) => {
|
||||
// handler for sending http request
|
||||
ipcMain.handle('send-http-request', async (event, item, collectionUid, collectionPath, environment, collectionVariables) => {
|
||||
const cancelTokenUid = uuid();
|
||||
const requestUid = uuid();
|
||||
|
||||
const onConsoleLog = (type, args) => {
|
||||
console[type](...args);
|
||||
|
||||
mainWindow.webContents.send('main:console-log', {
|
||||
type,
|
||||
args
|
||||
});
|
||||
};
|
||||
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'request-queued',
|
||||
requestUid,
|
||||
collectionUid,
|
||||
itemUid: item.uid,
|
||||
cancelTokenUid
|
||||
});
|
||||
|
||||
const _request = item.draft ? item.draft.request : item.request;
|
||||
const request = prepareRequest(_request);
|
||||
const envVars = getEnvVars(environment);
|
||||
|
||||
try {
|
||||
mainWindow.webContents.send('main:http-request-queued', {
|
||||
collectionUid,
|
||||
itemUid: item.uid,
|
||||
cancelTokenUid
|
||||
});
|
||||
|
||||
const _request = item.draft ? item.draft.request : item.request;
|
||||
const request = prepareRequest(_request);
|
||||
|
||||
// make axios work in node using form data
|
||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||
if(request.headers && request.headers['content-type'] === 'multipart/form-data') {
|
||||
@@ -94,8 +115,6 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
request.cancelToken = cancelToken.token;
|
||||
saveCancelToken(cancelTokenUid, cancelToken);
|
||||
|
||||
const envVars = getEnvVars(environment);
|
||||
|
||||
// run pre-request vars
|
||||
const preRequestVars = get(request, 'vars.req', []);
|
||||
if(preRequestVars && preRequestVars.length) {
|
||||
@@ -105,6 +124,7 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: result.envVariables,
|
||||
collectionVariables: result.collectionVariables,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
@@ -113,21 +133,28 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
const requestScript = get(request, 'script.req');
|
||||
if(requestScript && requestScript.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = await scriptRuntime.runRequestScript(requestScript, request, envVars, collectionVariables, collectionPath);
|
||||
const result = await scriptRuntime.runRequestScript(requestScript, request, envVars, collectionVariables, collectionPath, onConsoleLog);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: result.envVariables,
|
||||
collectionVariables: result.collectionVariables,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
interpolateVars(request, envVars, collectionVariables);
|
||||
|
||||
// stringify the request url encoded params
|
||||
if(request.headers['content-type'] === 'application/x-www-form-urlencoded') {
|
||||
request.data = qs.stringify(request.data);
|
||||
}
|
||||
|
||||
// todo:
|
||||
// i have no clue why electron can't send the request object
|
||||
// without safeParseJSON(safeStringifyJSON(request.data))
|
||||
mainWindow.webContents.send('main:http-request-sent', {
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'request-sent',
|
||||
requestSent: {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
@@ -136,9 +163,36 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
},
|
||||
collectionUid,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
cancelTokenUid
|
||||
});
|
||||
|
||||
const preferences = getPreferences();
|
||||
const sslVerification = get(preferences, 'request.sslVerification', true);
|
||||
const httpsAgentRequestFields = {};
|
||||
if(!sslVerification) {
|
||||
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
||||
}
|
||||
else {
|
||||
const cacertArray = [preferences['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS];
|
||||
cacertFile = cacertArray.find(el => el);
|
||||
if(cacertFile && cacertFile.length > 1) {
|
||||
try {
|
||||
const fs = require('fs');
|
||||
caCrt = fs.readFileSync(cacertFile);
|
||||
httpsAgentRequestFields['ca'] = caCrt;
|
||||
} catch(err) {
|
||||
console.log('Error reading CA cert file:' + cacertFile, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(Object.keys(httpsAgentRequestFields).length > 0) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
...httpsAgentRequestFields
|
||||
});
|
||||
}
|
||||
|
||||
const response = await axios(request);
|
||||
|
||||
// run post-response vars
|
||||
@@ -150,6 +204,7 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: result.envVariables,
|
||||
collectionVariables: result.collectionVariables,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
@@ -158,35 +213,49 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
const responseScript = get(request, 'script.res');
|
||||
if(responseScript && responseScript.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = scriptRuntime.runResponseScript(responseScript, request, response, envVars, collectionVariables, collectionPath);
|
||||
const result = await scriptRuntime.runResponseScript(responseScript, request, response, envVars, collectionVariables, collectionPath, onConsoleLog);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: result.envVariables,
|
||||
collectionVariables: result.collectionVariables,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// run assertions
|
||||
const assertions = get(request, 'assertions');
|
||||
const assertRuntime = new AssertRuntime();
|
||||
const results = assertRuntime.runAssertions(assertions, request, response, envVars, collectionVariables, collectionPath);
|
||||
if(assertions && assertions.length) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
const results = assertRuntime.runAssertions(assertions, request, response, envVars, collectionVariables, collectionPath);
|
||||
|
||||
mainWindow.webContents.send('main:assertion-results', {
|
||||
results: results,
|
||||
itemUid: item.uid,
|
||||
collectionUid
|
||||
});
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'assertion-results',
|
||||
results: results,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// run tests
|
||||
const testFile = get(item, 'request.tests');
|
||||
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
||||
if(testFile && testFile.length) {
|
||||
const testRuntime = new TestRuntime();
|
||||
const result = testRuntime.runTests(testFile, request, response, envVars, collectionVariables, collectionPath);
|
||||
const testResults = testRuntime.runTests(testFile, request, response, envVars, collectionVariables, collectionPath, onConsoleLog);
|
||||
|
||||
mainWindow.webContents.send('main:test-results', {
|
||||
results: result.results,
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'test-results',
|
||||
results: testResults.results,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testResults.envVariables,
|
||||
collectionVariables: testResults.collectionVariables,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
@@ -204,8 +273,51 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
// need to convey the error to the UI
|
||||
// and need not be always a network error
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
|
||||
if (axios.isCancel(error)) {
|
||||
let error = new Error("Request cancelled");
|
||||
error.isCancel = true;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if(error.response) {
|
||||
if(error && error.response) {
|
||||
// run assertions
|
||||
const assertions = get(request, 'assertions');
|
||||
if(assertions && assertions.length) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
const results = assertRuntime.runAssertions(assertions, request, error.response, envVars, collectionVariables, collectionPath);
|
||||
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'assertion-results',
|
||||
results: results,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// run tests
|
||||
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
||||
if(testFile && testFile.length) {
|
||||
const testRuntime = new TestRuntime();
|
||||
const testResults = testRuntime.runTests(testFile, request, error.response, envVars, collectionVariables, collectionPath, onConsoleLog);
|
||||
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'test-results',
|
||||
results: testResults.results,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testResults.envVariables,
|
||||
collectionVariables: testResults.collectionVariables,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
@@ -235,6 +347,15 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
const envVars = getEnvVars(environment);
|
||||
const request = prepareGqlIntrospectionRequest(endpoint, envVars);
|
||||
|
||||
const preferences = getPreferences();
|
||||
const sslVerification = get(preferences, 'request.sslVerification', true);
|
||||
|
||||
if(!sslVerification) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
}
|
||||
|
||||
const response = await axios(request);
|
||||
|
||||
return {
|
||||
@@ -262,6 +383,15 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
const collectionPath = collection.pathname;
|
||||
const folderUid = folder ? folder.uid : null;
|
||||
|
||||
const onConsoleLog = (type, args) => {
|
||||
console[type](...args);
|
||||
|
||||
mainWindow.webContents.send('main:console-log', {
|
||||
type,
|
||||
args
|
||||
});
|
||||
};
|
||||
|
||||
if(!folder) {
|
||||
folder = collection;
|
||||
}
|
||||
@@ -304,15 +434,15 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
let timeStart;
|
||||
let timeEnd;
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'request-queued',
|
||||
...eventData
|
||||
});
|
||||
|
||||
const _request = item.draft ? item.draft.request : item.request;
|
||||
const request = prepareRequest(_request);
|
||||
|
||||
try {
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'request-queued',
|
||||
...eventData
|
||||
});
|
||||
|
||||
const _request = item.draft ? item.draft.request : item.request;
|
||||
const request = prepareRequest(_request);
|
||||
|
||||
// make axios work in node using form data
|
||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||
if(request.headers && request.headers['content-type'] === 'multipart/form-data') {
|
||||
@@ -335,7 +465,7 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
const requestScript = get(request, 'script.req');
|
||||
if(requestScript && requestScript.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = scriptRuntime.runRequestScript(requestScript, request, envVars, collectionVariables, collectionPath);
|
||||
const result = await scriptRuntime.runRequestScript(requestScript, request, envVars, collectionVariables, collectionPath, onConsoleLog);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: result.envVariables,
|
||||
@@ -361,6 +491,15 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
...eventData
|
||||
});
|
||||
|
||||
const preferences = getPreferences();
|
||||
const sslVerification = get(preferences, 'request.sslVerification', true);
|
||||
|
||||
if(!sslVerification) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
}
|
||||
|
||||
// send request
|
||||
timeStart = Date.now();
|
||||
const response = await axios(request);
|
||||
@@ -383,7 +522,7 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
const responseScript = get(request, 'script.res');
|
||||
if(responseScript && responseScript.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = scriptRuntime.runResponseScript(responseScript, request, response, envVars, collectionVariables, collectionPath);
|
||||
const result = await scriptRuntime.runResponseScript(responseScript, request, response, envVars, collectionVariables, collectionPath, onConsoleLog);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: result.envVariables,
|
||||
@@ -407,16 +546,22 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
}
|
||||
|
||||
// run tests
|
||||
const testFile = get(item, 'request.tests');
|
||||
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
||||
if(testFile && testFile.length) {
|
||||
const testRuntime = new TestRuntime();
|
||||
const result = testRuntime.runTests(testFile, request, response, envVars, collectionVariables, collectionPath);
|
||||
const testResults = testRuntime.runTests(testFile, request, response, envVars, collectionVariables, collectionPath, onConsoleLog);
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'test-results',
|
||||
testResults: result.results,
|
||||
testResults: testResults.results,
|
||||
...eventData
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testResults.envVariables,
|
||||
collectionVariables: testResults.collectionVariables,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
@@ -448,7 +593,51 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
size: error.response.headers['content-length'] || getSize(error.response.data),
|
||||
data: error.response.data,
|
||||
}
|
||||
|
||||
// run assertions
|
||||
const assertions = get(item, 'request.assertions');
|
||||
if(assertions && assertions.length) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
const results = assertRuntime.runAssertions(assertions, request, error.response, envVars, collectionVariables, collectionPath);
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'assertion-results',
|
||||
assertionResults: results,
|
||||
itemUid: item.uid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// run tests
|
||||
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
||||
if(testFile && testFile.length) {
|
||||
const testRuntime = new TestRuntime();
|
||||
const testResults = testRuntime.runTests(testFile, request, error.response, envVars, collectionVariables, collectionPath, onConsoleLog);
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'test-results',
|
||||
testResults: testResults.results,
|
||||
...eventData
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testResults.envVariables,
|
||||
collectionVariables: testResults.collectionVariables,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// if we get a response from the server, we consider it as a success
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'response-received',
|
||||
error: error ? error.message : 'An error occurred while running the request',
|
||||
responseReceived: responseReceived,
|
||||
...eventData
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'error',
|
||||
error: error ? error.message : 'An error occurred while running the request',
|
||||
|
||||
@@ -42,6 +42,17 @@ const interpolateVars = (request, envVars = {}, collectionVariables ={}) => {
|
||||
request.data = interpolate(request.data);
|
||||
}
|
||||
}
|
||||
} else if(request.headers["content-type"] === "application/x-www-form-urlencoded") {
|
||||
if(typeof request.data === "object") {
|
||||
try {
|
||||
let parsed = JSON.stringify(request.data);
|
||||
parsed = interpolate(parsed);
|
||||
request.data = JSON.parse(parsed);
|
||||
} catch (err) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
request.data = interpolate(request.data);
|
||||
}
|
||||
|
||||
each(request.params, (param) => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const { get, each, filter } = require('lodash');
|
||||
const qs = require('qs');
|
||||
|
||||
const prepareRequest = (request) => {
|
||||
const headers = {};
|
||||
@@ -39,7 +38,7 @@ const prepareRequest = (request) => {
|
||||
const params = {};
|
||||
const enabledParams = filter(request.body.formUrlEncoded, (p) => p.enabled);
|
||||
each(enabledParams, (p) => (params[p.name] = p.value));
|
||||
axiosRequest.data = qs.stringify(params);
|
||||
axiosRequest.data = params;
|
||||
}
|
||||
|
||||
if (request.body.mode === 'multipartForm') {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/js",
|
||||
"version": "0.2.0",
|
||||
"version": "0.4.0",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
@@ -16,12 +16,14 @@
|
||||
"@usebruno/query": "0.1.0",
|
||||
"ajv": "^8.12.0",
|
||||
"atob": "^2.1.2",
|
||||
"axios": "^0.26.0",
|
||||
"btoa": "^1.2.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"json-query": "^2.2.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "3.3.4",
|
||||
"node-fetch": "2.*",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
|
||||
class Bru {
|
||||
constructor(envVariables, collectionVariables) {
|
||||
this._envVariables = envVariables;
|
||||
this._collectionVariables = collectionVariables;
|
||||
this.envVariables = envVariables;
|
||||
this.collectionVariables = collectionVariables;
|
||||
}
|
||||
|
||||
getEnvName() {
|
||||
return this.envVariables.__name__;
|
||||
}
|
||||
|
||||
getProcessEnv(key) {
|
||||
return process.env[key];
|
||||
}
|
||||
|
||||
getEnvVar(key) {
|
||||
return this._envVariables[key];
|
||||
return this.envVariables[key];
|
||||
}
|
||||
|
||||
setEnvVar(key, value) {
|
||||
@@ -15,11 +23,11 @@ class Bru {
|
||||
}
|
||||
|
||||
// gracefully ignore if key is not present in environment
|
||||
if(!this._envVariables.hasOwnProperty(key)) {
|
||||
return;
|
||||
if(!this.envVariables.hasOwnProperty(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._envVariables[key] = value;
|
||||
this.envVariables[key] = value;
|
||||
}
|
||||
|
||||
setVar(key, value) {
|
||||
@@ -27,12 +35,12 @@ class Bru {
|
||||
throw new Error('Key is required');
|
||||
}
|
||||
|
||||
this._collectionVariables[key] = value;
|
||||
this.collectionVariables[key] = value;
|
||||
}
|
||||
|
||||
getVar(key) {
|
||||
return this._collectionVariables[key];
|
||||
return this.collectionVariables[key];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Bru;
|
||||
module.exports = Bru;
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
class BrunoResponse {
|
||||
constructor(res) {
|
||||
this.res = res;
|
||||
this.status = res.status;
|
||||
this.statusText = res.statusText;
|
||||
this.headers = res.headers;
|
||||
this.body = res.data;
|
||||
this.status = res ? res.status : null;
|
||||
this.statusText = res ? res.statusText : null;
|
||||
this.headers = res ? res.headers : null;
|
||||
this.body = res ? res.data : null;
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.res.status;
|
||||
return this.res ? this.res.status : null;
|
||||
}
|
||||
|
||||
getHeader(name) {
|
||||
return this.res.header[name];
|
||||
return (this.res && this.res.headers) ? this.res.headers[name] : null;
|
||||
}
|
||||
|
||||
getHeaders() {
|
||||
return this.res.headers;
|
||||
return this.res ? this.res.headers : null;
|
||||
}
|
||||
|
||||
getBody() {
|
||||
return this.res.data;
|
||||
return this.res ? this.res.data : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,13 +19,14 @@ const moment = require('moment');
|
||||
const uuid = require('uuid');
|
||||
const nanoid = require('nanoid');
|
||||
const axios = require('axios');
|
||||
const fetch = require('node-fetch');
|
||||
const CryptoJS = require('crypto-js');
|
||||
|
||||
class ScriptRuntime {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
async runRequestScript(script, request, envVariables, collectionVariables, collectionPath) {
|
||||
async runRequestScript(script, request, envVariables, collectionVariables, collectionPath, onConsoleLog){
|
||||
const bru = new Bru(envVariables, collectionVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
|
||||
@@ -33,6 +34,21 @@ class ScriptRuntime {
|
||||
bru,
|
||||
req
|
||||
};
|
||||
|
||||
if(onConsoleLog && typeof onConsoleLog === 'function') {
|
||||
const customLogger = (type) => {
|
||||
return (...args) => {
|
||||
onConsoleLog(type, args);
|
||||
}
|
||||
};
|
||||
context.console = {
|
||||
log: customLogger('log'),
|
||||
info: customLogger('info'),
|
||||
warn: customLogger('warn'),
|
||||
error: customLogger('error')
|
||||
}
|
||||
}
|
||||
|
||||
const vm = new NodeVM({
|
||||
sandbox: context,
|
||||
require: {
|
||||
@@ -57,20 +73,13 @@ class ScriptRuntime {
|
||||
uuid,
|
||||
nanoid,
|
||||
axios,
|
||||
'node-fetch': fetch,
|
||||
'crypto-js': CryptoJS
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// wrap script inside a async function that gets called
|
||||
script = `return (async () => { ${script} })()`;
|
||||
|
||||
// bug that needs to be fixed
|
||||
// vm.run is not awaiting the async function
|
||||
// created an issue in vm2 repo: https://github.com/patriksimek/vm2/issues/513
|
||||
const result = await vm.run(script, path.join(collectionPath, 'vm.js'));
|
||||
console.log(result);
|
||||
|
||||
const asyncVM = vm.run(`module.exports = async () => { ${script} }`, path.join(collectionPath, 'vm.js'));
|
||||
await asyncVM();
|
||||
return {
|
||||
request,
|
||||
envVariables,
|
||||
@@ -78,7 +87,7 @@ class ScriptRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
runResponseScript(script, request, response, envVariables, collectionVariables, collectionPath) {
|
||||
async runResponseScript(script, request, response, envVariables, collectionVariables, collectionPath, onConsoleLog){
|
||||
const bru = new Bru(envVariables, collectionVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
@@ -88,6 +97,21 @@ class ScriptRuntime {
|
||||
req,
|
||||
res
|
||||
};
|
||||
|
||||
if(onConsoleLog && typeof onConsoleLog === 'function') {
|
||||
const customLogger = (type) => {
|
||||
return (...args) => {
|
||||
onConsoleLog(type, args);
|
||||
}
|
||||
};
|
||||
context.console = {
|
||||
log: customLogger('log'),
|
||||
info: customLogger('info'),
|
||||
warn: customLogger('warn'),
|
||||
error: customLogger('error')
|
||||
}
|
||||
}
|
||||
|
||||
const vm = new NodeVM({
|
||||
sandbox: context,
|
||||
require: {
|
||||
@@ -101,12 +125,15 @@ class ScriptRuntime {
|
||||
moment,
|
||||
uuid,
|
||||
nanoid,
|
||||
axios,
|
||||
'node-fetch': fetch,
|
||||
'crypto-js': CryptoJS
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
vm.run(script, path.join(collectionPath, 'vm.js'));
|
||||
const asyncVM = vm.run(`module.exports = async () => { ${script} }`, path.join(collectionPath, 'vm.js'));
|
||||
await asyncVM();
|
||||
|
||||
return {
|
||||
response,
|
||||
|
||||
@@ -20,7 +20,7 @@ class TestRuntime {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
runTests(testsFile, request, response, envVariables, collectionVariables, collectionPath) {
|
||||
runTests(testsFile, request, response, envVariables, collectionVariables, collectionPath, onConsoleLog){
|
||||
const bru = new Bru(envVariables, collectionVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
@@ -28,6 +28,15 @@ class TestRuntime {
|
||||
const __brunoTestResults = new TestResults();
|
||||
const test = Test(__brunoTestResults, chai);
|
||||
|
||||
if(!testsFile || !testsFile.length) {
|
||||
return {
|
||||
request,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
results: __brunoTestResults.getResults()
|
||||
};
|
||||
}
|
||||
|
||||
const context = {
|
||||
test,
|
||||
bru,
|
||||
@@ -38,6 +47,20 @@ class TestRuntime {
|
||||
__brunoTestResults: __brunoTestResults
|
||||
};
|
||||
|
||||
if(onConsoleLog && typeof onConsoleLog === 'function') {
|
||||
const customLogger = (type) => {
|
||||
return (...args) => {
|
||||
onConsoleLog(type, args);
|
||||
}
|
||||
};
|
||||
context.console = {
|
||||
log: customLogger('log'),
|
||||
info: customLogger('info'),
|
||||
warn: customLogger('warn'),
|
||||
error: customLogger('error')
|
||||
}
|
||||
}
|
||||
|
||||
const vm = new NodeVM({
|
||||
sandbox: context,
|
||||
require: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/lang",
|
||||
"version": "0.2.2",
|
||||
"version": "0.3.0",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
|
||||
@@ -20,6 +20,14 @@ You can use git or any version control of your choice to collaborate over your a
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Run across multiple platforms 🖥️
|
||||
 <br /><br />
|
||||
|
||||
### Collaborate via Git 👩💻🧑💻
|
||||
Or any version control system of your choice
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Website 📄
|
||||
Please visit [here](https://www.usebruno.com) to checkout our website and download the app
|
||||
|
||||
|
||||
Reference in New Issue
Block a user