mirror of
https://github.com/usebruno/bruno.git
synced 2025-02-01 18:39:34 +01:00
Merge remote-tracking branch 'origin/main' into feat/log-client-cert-status
This commit is contained in:
commit
fc54bbcdd1
10
.github/workflows/npm-bru-cli.yml
vendored
10
.github/workflows/npm-bru-cli.yml
vendored
@ -2,6 +2,11 @@ name: Bru CLI Tests (npm)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build:
|
||||
description: 'Test Bru CLI (npm)'
|
||||
required: true
|
||||
default: 'true'
|
||||
|
||||
# Assign permissions for unit tests to be reported.
|
||||
# See https://github.com/dorny/test-reporter/issues/168
|
||||
@ -15,7 +20,10 @@ permissions:
|
||||
jobs:
|
||||
test:
|
||||
name: CLI Tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
|
7893
package-lock.json
generated
7893
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -30,6 +30,7 @@
|
||||
"ts-jest": "^29.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "node ./scripts/setup.js",
|
||||
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
@ -51,6 +52,6 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup":"3.29.5"
|
||||
"rollup": "3.29.5"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"presets": ["next/babel"],
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": [["styled-components", { "ssr": true }]]
|
||||
}
|
2
packages/bruno-app/.gitignore
vendored
2
packages/bruno-app/.gitignore
vendored
@ -31,6 +31,6 @@ yarn-error.log*
|
||||
|
||||
# next.js
|
||||
.next/
|
||||
out/
|
||||
dist/
|
||||
|
||||
.env
|
@ -1,22 +0,0 @@
|
||||
module.exports = {
|
||||
output: 'export',
|
||||
reactStrictMode: false,
|
||||
publicRuntimeConfig: {
|
||||
CI: process.env.CI,
|
||||
PLAYWRIGHT: process.env.PLAYWRIGHT,
|
||||
ENV: process.env.ENV
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
// Fixes npm packages that depend on `fs` module
|
||||
if (!isServer) {
|
||||
config.resolve.fallback.fs = false;
|
||||
}
|
||||
Object.defineProperty(config, 'devtool', {
|
||||
get() {
|
||||
return 'source-map';
|
||||
},
|
||||
set() {},
|
||||
});
|
||||
return config;
|
||||
},
|
||||
};
|
@ -3,15 +3,15 @@
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cross-env ENV=dev next dev -p 3000",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"dev": "rsbuild dev",
|
||||
"build": "rsbuild build -m production",
|
||||
"preview": "rsbuild preview",
|
||||
"test": "jest",
|
||||
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
"@prantlf/jsonlint": "^16.0.0",
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
@ -35,21 +35,20 @@
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"httpsnippet": "^3.0.6",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next": "24.1.2",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"jsesc": "^3.0.2",
|
||||
"jshint": "^2.13.6",
|
||||
"json5": "^2.2.3",
|
||||
"jsonc-parser": "^3.2.1",
|
||||
"jsonpath-plus": "10.1.0",
|
||||
"jsonpath-plus": "10.2.0",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.2",
|
||||
"markdown-it-replace-link": "^1.2.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "14.2.16",
|
||||
"nanoid": "3.3.8",
|
||||
"path": "^0.12.7",
|
||||
"pdfjs-dist": "4.4.168",
|
||||
"platform": "^1.3.6",
|
||||
@ -57,17 +56,17 @@
|
||||
"prettier": "^2.7.1",
|
||||
"qs": "^6.11.0",
|
||||
"query-string": "^7.0.1",
|
||||
"react": "18.2.0",
|
||||
"react": "19.0.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-inspector": "^6.0.2",
|
||||
"react-pdf": "9.1.1",
|
||||
"react-player": "^2.16.0",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
"strip-json-comments": "^5.0.1",
|
||||
@ -79,13 +78,14 @@
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/plugin-transform-spread": "^7.16.7",
|
||||
"@babel/preset-env": "^7.16.4",
|
||||
"@babel/preset-react": "^7.16.0",
|
||||
"@babel/runtime": "^7.16.3",
|
||||
"@rsbuild/core": "^1.1.2",
|
||||
"@rsbuild/plugin-babel": "^1.0.3",
|
||||
"@rsbuild/plugin-node-polyfill": "^1.2.0",
|
||||
"@rsbuild/plugin-react": "^1.0.7",
|
||||
"@rsbuild/plugin-sass": "^1.1.0",
|
||||
"@rsbuild/plugin-styled-components": "1.1.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "7.1.2",
|
||||
"file-loader": "^6.2.0",
|
||||
|
27
packages/bruno-app/rsbuild.config.mjs
Normal file
27
packages/bruno-app/rsbuild.config.mjs
Normal file
@ -0,0 +1,27 @@
|
||||
import { defineConfig } from '@rsbuild/core';
|
||||
import { pluginReact } from '@rsbuild/plugin-react';
|
||||
import { pluginBabel } from '@rsbuild/plugin-babel';
|
||||
import { pluginStyledComponents } from '@rsbuild/plugin-styled-components';
|
||||
import { pluginSass } from '@rsbuild/plugin-sass';
|
||||
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
pluginNodePolyfill(),
|
||||
pluginReact(),
|
||||
pluginStyledComponents(),
|
||||
pluginSass(),
|
||||
pluginBabel({
|
||||
include: /\.(?:js|jsx|tsx)$/,
|
||||
babelLoaderOptions(opts) {
|
||||
opts.plugins?.unshift('babel-plugin-react-compiler');
|
||||
}
|
||||
})
|
||||
],
|
||||
source: {
|
||||
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file
|
||||
},
|
||||
html: {
|
||||
title: 'Bruno'
|
||||
},
|
||||
});
|
@ -76,7 +76,11 @@ if (!SERVER_RENDERED) {
|
||||
'bru.getRequestVar(key)',
|
||||
'bru.sleep(ms)',
|
||||
'bru.getGlobalEnvVar(key)',
|
||||
'bru.setGlobalEnvVar(key, value)'
|
||||
'bru.setGlobalEnvVar(key, value)',
|
||||
'bru.runner',
|
||||
'bru.runner.setNextRequest(requestName)',
|
||||
'bru.runner.skipRequest()',
|
||||
'bru.runner.stopExecution()'
|
||||
];
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
const cursor = editor.getCursor();
|
||||
@ -98,7 +102,7 @@ if (!SERVER_RENDERED) {
|
||||
if (curWordBru) {
|
||||
hintWords.forEach((h) => {
|
||||
if (h.includes('.') == curWordBru.includes('.') && h.startsWith(curWordBru)) {
|
||||
result.list.push(curWordBru.includes('.') ? h.split('.')[1] : h);
|
||||
result.list.push(curWordBru.includes('.') ? h.split('.')?.at(-1) : h);
|
||||
}
|
||||
});
|
||||
result.list?.sort();
|
||||
|
@ -68,12 +68,13 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
});
|
||||
|
||||
const getFile = (e) => {
|
||||
if (e.files?.[0]?.path) {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]);
|
||||
if (filePath) {
|
||||
let relativePath;
|
||||
if (isWindowsOS()) {
|
||||
relativePath = slash(path.win32.relative(root, e.files[0].path));
|
||||
relativePath = slash(path.win32.relative(root, filePath));
|
||||
} else {
|
||||
relativePath = path.posix.relative(root, e.files[0].path);
|
||||
relativePath = path.posix.relative(root, filePath);
|
||||
}
|
||||
formik.setFieldValue(e.name, relativePath);
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="current-environment flex items-center justify-center pl-3 pr-2 py-1 select-none">
|
||||
{activeEnvironment ? activeEnvironment.name : 'No Environment'}
|
||||
<p className="text-nowrap truncate max-w-32">{activeEnvironment ? activeEnvironment.name : 'No Environment'}</p>
|
||||
<IconCaretDown className="caret" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
|
@ -10,6 +10,11 @@ import Modal from 'components/Modal';
|
||||
const CreateEnvironment = ({ collection, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
|
||||
const validateEnvironmentName = (name) => {
|
||||
return !collection?.environments?.some((env) => env?.name?.toLowerCase().trim() === name?.toLowerCase().trim());
|
||||
};
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
@ -17,9 +22,10 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
.min(1, 'Must be at least 1 character')
|
||||
.max(50, 'Must be 50 characters or less')
|
||||
.required('Name is required')
|
||||
.test('duplicate-name', 'Environment already exists', validateEnvironmentName)
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(addEnvironment(values.name, collection.uid))
|
||||
|
@ -23,6 +23,10 @@ const StyledWrapper = styled.div`
|
||||
padding: 8px 10px;
|
||||
border-left: solid 2px transparent;
|
||||
text-decoration: none;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
@ -8,6 +8,7 @@ import ImportEnvironment from '../ImportEnvironment';
|
||||
import ManageSecrets from '../ManageSecrets';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ConfirmSwitchEnv from './ConfirmSwitchEnv';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
|
||||
const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified }) => {
|
||||
const { environments } = collection;
|
||||
@ -103,13 +104,15 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
{environments &&
|
||||
environments.length &&
|
||||
environments.map((env) => (
|
||||
<div
|
||||
key={env.uid}
|
||||
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
|
||||
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks
|
||||
>
|
||||
<span className="break-all">{env.name}</span>
|
||||
</div>
|
||||
<ToolHint key={env.uid} text={env.name} toolhintId={env.uid} place="right">
|
||||
<div
|
||||
id={env.uid}
|
||||
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
|
||||
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks
|
||||
>
|
||||
<span className="break-all">{env.name}</span>
|
||||
</div>
|
||||
</ToolHint>
|
||||
))}
|
||||
<div className="btn-create-environment" onClick={() => handleCreateEnvClick()}>
|
||||
+ <span>Create</span>
|
||||
|
@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.editing-mode {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -0,0 +1,63 @@
|
||||
import 'github-markdown-css/github-markdown.css';
|
||||
import get from 'lodash/get';
|
||||
import { updateFolderDocs } from 'providers/ReduxStore/slices/collections';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Markdown from 'components/MarkDown';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Documentation = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const docs = get(folder, 'root.docs', '');
|
||||
|
||||
const toggleViewMode = () => {
|
||||
setIsEditing((prev) => !prev);
|
||||
};
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateFolderDocs({
|
||||
folderUid: folder.uid,
|
||||
collectionUid: collection.uid,
|
||||
docs: value
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
|
||||
if (!folder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col gap-y-1 h-full w-full relative">
|
||||
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
|
||||
{isEditing ? 'Preview' : 'Edit'}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
value={docs || ''}
|
||||
onEdit={onEdit}
|
||||
onSave={onSave}
|
||||
mode="application/text"
|
||||
/>
|
||||
) : (
|
||||
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Documentation;
|
@ -7,6 +7,7 @@ import Script from './Script';
|
||||
import Tests from './Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars';
|
||||
import Documentation from './Documentation';
|
||||
import DotIcon from 'components/Icons/Dot';
|
||||
|
||||
const ContentIndicator = () => {
|
||||
@ -60,6 +61,9 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
case 'vars': {
|
||||
return <Vars collection={collection} folder={folder} />;
|
||||
}
|
||||
case 'docs': {
|
||||
return <Documentation collection={collection} folder={folder} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -89,6 +93,9 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
Vars
|
||||
{activeVarsCount > 0 && <sup className="ml-1 font-medium">{activeVarsCount}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
</div>
|
||||
<section className={`flex mt-4 h-full`}>{getTabPanel(tab)}</section>
|
||||
</div>
|
||||
|
@ -23,7 +23,7 @@ const EnvironmentSelector = () => {
|
||||
<ToolHint text="Global Environments" toolhintId="GlobalEnvironmentsToolhintId" className='flex flex-row'>
|
||||
<IconWorld className="globe" size={16} strokeWidth={1.5} />
|
||||
{
|
||||
activeEnvironment ? <div>{activeEnvironment?.name}</div> : null
|
||||
activeEnvironment ? <div className='text-nowrap truncate max-w-32'>{activeEnvironment?.name}</div> : null
|
||||
}
|
||||
</ToolHint>
|
||||
</div>
|
||||
|
@ -2,12 +2,19 @@ import React, { useEffect, useRef } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
|
||||
const CreateEnvironment = ({ onClose }) => {
|
||||
const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);
|
||||
|
||||
const validateEnvironmentName = (name) => {
|
||||
const trimmedName = name?.toLowerCase().trim();
|
||||
return globalEnvs.every((env) => env?.name?.toLowerCase().trim() !== trimmedName);
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const formik = useFormik({
|
||||
@ -17,9 +24,10 @@ const CreateEnvironment = ({ onClose }) => {
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
.min(1, 'Must be at least 1 character')
|
||||
.max(50, 'Must be 50 characters or less')
|
||||
.required('Name is required')
|
||||
.test('duplicate-name', 'Global Environment already exists', validateEnvironmentName)
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(addGlobalEnvironment({ name: values.name }))
|
||||
|
@ -23,6 +23,10 @@ const StyledWrapper = styled.div`
|
||||
padding: 8px 10px;
|
||||
border-left: solid 2px transparent;
|
||||
text-decoration: none;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
@ -8,6 +8,7 @@ import ConfirmSwitchEnv from './ConfirmSwitchEnv';
|
||||
import ManageSecrets from 'components/Environments/EnvironmentSettings/ManageSecrets/index';
|
||||
import ImportEnvironment from '../ImportEnvironment';
|
||||
import { isEqual } from 'lodash';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
|
||||
const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironment, setSelectedEnvironment, isModified, setIsModified }) => {
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
@ -112,13 +113,15 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
|
||||
{environments &&
|
||||
environments.length &&
|
||||
environments.map((env) => (
|
||||
<div
|
||||
key={env.uid}
|
||||
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
|
||||
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks
|
||||
>
|
||||
<span className="break-all">{env.name}</span>
|
||||
</div>
|
||||
<ToolHint key={env.uid} text={env.name} toolhintId={env.uid} place="right">
|
||||
<div
|
||||
id={env.uid}
|
||||
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
|
||||
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle click
|
||||
>
|
||||
<span className="break-all">{env.name}</span>
|
||||
</div>
|
||||
</ToolHint>
|
||||
))}
|
||||
<div className="btn-create-environment" onClick={() => handleCreateEnvClick()}>
|
||||
+ <span>Create</span>
|
||||
|
@ -13,6 +13,12 @@ const StyledWrapper = styled.div`
|
||||
line-height: 30px;
|
||||
overflow: hidden;
|
||||
|
||||
pre.CodeMirror-placeholder {
|
||||
color: ${(props) => props.theme.text};
|
||||
padding-left: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
overflow: hidden !important;
|
||||
${'' /* padding-bottom: 50px !important; */}
|
||||
|
@ -30,6 +30,7 @@ class MultiLineEditor extends Component {
|
||||
lineWrapping: false,
|
||||
lineNumbers: false,
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
placeholder: this.props.placeholder,
|
||||
mode: 'brunovariables',
|
||||
brunoVarInfo: {
|
||||
variables
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
function Portal({ children, wrapperId }) {
|
||||
wrapperId = wrapperId || 'bruno-app-body';
|
||||
|
||||
return createPortal(children, document.getElementById(wrapperId));
|
||||
function Portal({ children }) {
|
||||
return createPortal(children, document.body);
|
||||
}
|
||||
export default Portal;
|
||||
|
@ -90,7 +90,10 @@ const General = ({ close }) => {
|
||||
};
|
||||
|
||||
const addCaCertificate = (e) => {
|
||||
formik.setFieldValue('customCaCertificate.filePath', e.target.files[0]?.path);
|
||||
const filePath = window?.ipcRenderer?.getFilePath(e?.target?.files?.[0]);
|
||||
if (filePath) {
|
||||
formik.setFieldValue('customCaCertificate.filePath', filePath);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteCaCertificate = () => {
|
||||
|
@ -20,6 +20,7 @@ import React from 'react';
|
||||
* endsWith : ends with
|
||||
* between : between
|
||||
* isEmpty : is empty
|
||||
* isNotEmpty : is not empty
|
||||
* isNull : is null
|
||||
* isUndefined : is undefined
|
||||
* isDefined : is defined
|
||||
@ -51,6 +52,7 @@ const AssertionOperator = ({ operator, onChange }) => {
|
||||
'endsWith',
|
||||
'between',
|
||||
'isEmpty',
|
||||
'isNotEmpty',
|
||||
'isNull',
|
||||
'isUndefined',
|
||||
'isDefined',
|
||||
|
@ -24,6 +24,7 @@ import { useTheme } from 'providers/Theme';
|
||||
* endsWith : ends with
|
||||
* between : between
|
||||
* isEmpty : is empty
|
||||
* isNotEmpty : is not empty
|
||||
* isNull : is null
|
||||
* isUndefined : is undefined
|
||||
* isDefined : is defined
|
||||
@ -61,6 +62,7 @@ const parseAssertionOperator = (str = '') => {
|
||||
'endsWith',
|
||||
'between',
|
||||
'isEmpty',
|
||||
'isNotEmpty',
|
||||
'isNull',
|
||||
'isUndefined',
|
||||
'isDefined',
|
||||
@ -75,6 +77,7 @@ const parseAssertionOperator = (str = '') => {
|
||||
|
||||
const unaryOperators = [
|
||||
'isEmpty',
|
||||
'isNotEmpty',
|
||||
'isNull',
|
||||
'isUndefined',
|
||||
'isDefined',
|
||||
@ -87,7 +90,7 @@ const parseAssertionOperator = (str = '') => {
|
||||
'isArray'
|
||||
];
|
||||
|
||||
const [operator, ...rest] = str.trim().split(' ');
|
||||
const [operator, ...rest] = str.split(' ');
|
||||
const value = rest.join(' ');
|
||||
|
||||
if (unaryOperators.includes(operator)) {
|
||||
@ -113,6 +116,7 @@ const parseAssertionOperator = (str = '') => {
|
||||
const isUnaryOperator = (operator) => {
|
||||
const unaryOperators = [
|
||||
'isEmpty',
|
||||
'isNotEmpty',
|
||||
'isNull',
|
||||
'isUndefined',
|
||||
'isDefined',
|
||||
@ -162,7 +166,7 @@ const AssertionRow = ({
|
||||
handleAssertionChange(
|
||||
{
|
||||
target: {
|
||||
value: `${op} ${value}`
|
||||
value: isUnaryOperator(op) ? op : `${op} ${value}`
|
||||
}
|
||||
},
|
||||
assertion,
|
||||
@ -178,7 +182,7 @@ const AssertionRow = ({
|
||||
theme={storedTheme}
|
||||
readOnly={true}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
onChange={(newValue) => {
|
||||
handleAssertionChange(
|
||||
{
|
||||
target: {
|
||||
@ -188,6 +192,7 @@ const AssertionRow = ({
|
||||
assertion,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
|
@ -20,8 +20,8 @@ const Wrapper = styled.div`
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
&:nth-child(2) {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
|
@ -24,7 +24,15 @@ const Wrapper = styled.div`
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,10 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
param.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'contentType': {
|
||||
param.contentType = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
break;
|
||||
@ -85,6 +89,7 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Content-Type</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -145,6 +150,27 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<MultiLineEditor
|
||||
onSave={onSave}
|
||||
theme={storedTheme}
|
||||
placeholder="Auto"
|
||||
value={param.contentType}
|
||||
onChange={(newValue) =>
|
||||
handleParamChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'contentType'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
|
@ -117,7 +117,6 @@ const QueryParams = ({ item, collection }) => {
|
||||
<StyledWrapper className="w-full flex flex-col absolute">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Query</div>
|
||||
|
||||
<Table
|
||||
headers={[
|
||||
{ name: 'Name', accessor: 'name', width: '31%' },
|
||||
@ -153,7 +152,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center justify-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
@ -188,7 +187,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
</code>
|
||||
</div>
|
||||
`}
|
||||
infotipId="path-param-InfoTip"
|
||||
infotipId="path-param-InfoTip"
|
||||
/>
|
||||
</div>
|
||||
<table>
|
||||
@ -241,11 +240,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
{!(pathParams && pathParams.length) ?
|
||||
<div className="title pr-2 py-3 mt-2 text-xs">
|
||||
|
||||
</div>
|
||||
: null}
|
||||
{!(pathParams && pathParams.length) ? <div className="title pr-2 py-3 mt-2 text-xs"></div> : null}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
@ -70,7 +70,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
|
||||
const handleGenerateCode = (e) => {
|
||||
e.stopPropagation();
|
||||
if (item.request.url !== '' || (item.draft?.request.url !== undefined && item.draft?.request.url !== '')) {
|
||||
if (item?.request?.url !== '' || (item.draft?.request?.url !== undefined && item.draft?.request?.url !== '')) {
|
||||
setGenerateCodeItemModalOpen(true);
|
||||
} else {
|
||||
toast.error('URL is required');
|
||||
|
@ -10,6 +10,7 @@ const StyledWrapper = styled.div`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 10px;
|
||||
min-width: 10px;
|
||||
padding: 0;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
|
@ -20,7 +20,7 @@ import { DocExplorer } from '@usebruno/graphql-docs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import SecuritySettings from 'components/SecuritySettings';
|
||||
import FolderSettings from 'components/FolderSettings';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
import { getGlobalEnvironmentVariables, getGlobalEnvironmentVariablesMasked } from 'utils/collections/index';
|
||||
import { produce } from 'immer';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
@ -39,13 +39,18 @@ const RequestTabPanel = () => {
|
||||
const _collections = useSelector((state) => state.collections.collections);
|
||||
|
||||
// merge `globalEnvironmentVariables` into the active collection and rebuild `collections` immer proxy object
|
||||
let collections = produce(_collections, draft => {
|
||||
let collections = produce(_collections, (draft) => {
|
||||
let collection = find(draft, (c) => c.uid === focusedTab?.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
// add selected global env variables to the collection object
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({
|
||||
globalEnvironments,
|
||||
activeGlobalEnvironmentUid
|
||||
});
|
||||
const globalEnvSecrets = getGlobalEnvironmentVariablesMasked({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
collection.globalEnvSecrets = globalEnvSecrets;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -20,14 +20,14 @@ const formatResponse = (data, mode, filter) => {
|
||||
}
|
||||
|
||||
if (data === null) {
|
||||
return data;
|
||||
return 'null';
|
||||
}
|
||||
|
||||
if (mode.includes('json')) {
|
||||
let isValidJSON = false;
|
||||
|
||||
try {
|
||||
isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object';
|
||||
isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object'
|
||||
} catch (error) {
|
||||
console.log('Error parsing JSON: ', error.message);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ const ResponseSave = ({ item }) => {
|
||||
const saveResponseToFile = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-response-to-file', response, item.requestSent.url)
|
||||
.invoke('renderer:save-response-to-file', response, item?.requestSent?.url)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error(get(err, 'error.message') || 'Something went wrong!');
|
||||
|
@ -43,7 +43,7 @@ const Timeline = ({ request, response }) => {
|
||||
|
||||
<div className="mt-4">
|
||||
<pre className="line response font-bold">
|
||||
<span className="arrow">{'<'}</span> {response.status} {response.statusText}
|
||||
<span className="arrow">{'<'}</span> {response.status} - {response.statusText}
|
||||
</pre>
|
||||
|
||||
{responseHeaders.map((h) => {
|
||||
|
@ -59,7 +59,7 @@ export default function RunnerResults({ collection }) {
|
||||
pathname: info.pathname,
|
||||
relativePath: getRelativePath(collection.pathname, info.pathname)
|
||||
};
|
||||
if (newItem.status !== 'error') {
|
||||
if (newItem.status !== 'error' && newItem.status !== 'skipped') {
|
||||
if (newItem.testResults) {
|
||||
const failed = newItem.testResults.filter((result) => result.status === 'fail');
|
||||
newItem.testStatus = failed.length ? 'fail' : 'pass';
|
||||
@ -163,29 +163,35 @@ export default function RunnerResults({ collection }) {
|
||||
<div className="pb-2 font-medium test-summary">
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
|
||||
</div>
|
||||
{runnerInfo?.statusText ?
|
||||
<div className="pb-2 font-medium danger">
|
||||
{runnerInfo?.statusText}
|
||||
</div>
|
||||
: null}
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<div key={item.uid}>
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{item.status !== 'error' && item.testStatus === 'pass' ? (
|
||||
{item.status !== 'error' && item.testStatus === 'pass' && item.status !== 'skipped' ? (
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
) : (
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`mr-1 ml-2 ${item.status == 'error' || item.testStatus == 'fail' ? 'danger' : ''}`}
|
||||
className={`mr-1 ml-2 ${item.status == 'error' || item.status == 'skipped' || item.testStatus == 'fail' ? 'danger' : ''}`}
|
||||
>
|
||||
{item.relativePath}
|
||||
</span>
|
||||
{item.status !== 'error' && item.status !== 'completed' ? (
|
||||
{item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? (
|
||||
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
|
||||
) : item.responseReceived?.status ? (
|
||||
<span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
(<span className="mr-1">{item.responseReceived?.status}</span>
|
||||
<span>{item.responseReceived?.statusText}</span>)
|
||||
<span className="mr-1">{item.responseReceived?.status}</span>
|
||||
-
|
||||
<span>{item.responseReceived?.statusText}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="danger text-xs cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
|
@ -12,10 +12,15 @@ const DeleteCollectionItem = ({ onClose, item, collection }) => {
|
||||
const isFolder = isItemAFolder(item);
|
||||
const onConfirm = () => {
|
||||
dispatch(deleteItem(item.uid, collection.uid)).then(() => {
|
||||
|
||||
if (isFolder) {
|
||||
// close all tabs that belong to the folder
|
||||
// including the folder itself and its children
|
||||
const tabUids = [...recursivelyGetAllItemUids(item.items), item.uid]
|
||||
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: recursivelyGetAllItemUids(item.items)
|
||||
tabUids: tabUids
|
||||
})
|
||||
);
|
||||
} else {
|
||||
|
@ -35,6 +35,28 @@ const StyledWrapper = styled.div`
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
|
||||
}
|
||||
}
|
||||
|
||||
.flexible-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.flexible-container {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 601px) and (max-width: 1200px) {
|
||||
.flexible-container {
|
||||
width: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1201px) {
|
||||
.flexible-container {
|
||||
width: 900px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
@ -48,7 +48,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
return (
|
||||
<Modal size="lg" title="Generate Code" handleCancel={onClose} hideFooter={true}>
|
||||
<StyledWrapper>
|
||||
<div className="flex w-full">
|
||||
<div className="flex w-full flexible-container">
|
||||
<div>
|
||||
<div className="generate-code-sidebar">
|
||||
{languages &&
|
||||
@ -59,7 +59,26 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
className={
|
||||
language.name === selectedLanguage.name ? 'generate-code-item active' : 'generate-code-item'
|
||||
}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSelectedLanguage(language)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Tab' || (e.shiftKey && e.key === 'Tab')) {
|
||||
e.preventDefault();
|
||||
const currentIndex = languages.findIndex((lang) => lang.name === selectedLanguage.name);
|
||||
const nextIndex = e.shiftKey
|
||||
? (currentIndex - 1 + languages.length) % languages.length
|
||||
: (currentIndex + 1) % languages.length;
|
||||
setSelectedLanguage(languages[nextIndex]);
|
||||
|
||||
// Explicitly focus on the new active element
|
||||
const nextElement = document.querySelector(`[data-language="${languages[nextIndex].name}"]`);
|
||||
nextElement?.focus();
|
||||
}
|
||||
|
||||
}}
|
||||
data-language={language.name}
|
||||
aria-pressed={language.name === selectedLanguage.name}
|
||||
>
|
||||
<span className="capitalize">{language.name}</span>
|
||||
</div>
|
||||
@ -69,6 +88,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
<div className="flex-grow p-4">
|
||||
{isValidUrl(finalUrl) ? (
|
||||
<CodeView
|
||||
tabIndex={-1}
|
||||
language={selectedLanguage}
|
||||
item={{
|
||||
...item,
|
||||
|
@ -6,6 +6,7 @@ import { useDispatch } from 'react-redux';
|
||||
import { isItemAFolder } from 'utils/tabs';
|
||||
import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
|
||||
const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -33,7 +34,8 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
}
|
||||
dispatch(renameItem(values.name, item.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Request renamed');
|
||||
isFolder && dispatch(closeTabs({ tabUids: [item.uid] }));
|
||||
toast.success(isFolder ? 'Folder renamed' : 'Request renamed');
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -128,13 +128,29 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
type: 'folder-settings'
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
collectionFolderClicked({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleFolderCollapse = () => {
|
||||
dispatch(
|
||||
collectionFolderClicked({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const handleRightClick = (event) => {
|
||||
const _menuDropdown = dropdownTippyRef.current;
|
||||
@ -183,7 +199,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const handleGenerateCode = (e) => {
|
||||
e.stopPropagation();
|
||||
dropdownTippyRef.current.hide();
|
||||
if (item.request.url !== '' || (item.draft?.request.url !== undefined && item.draft?.request.url !== '')) {
|
||||
if (item?.request?.url !== '' || (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')) {
|
||||
setGenerateCodeItemModalOpen(true);
|
||||
} else {
|
||||
toast.error('URL is required');
|
||||
@ -260,9 +276,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
})
|
||||
: null}
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className="flex flex-grow items-center h-full overflow-hidden"
|
||||
style={{
|
||||
paddingLeft: 8
|
||||
@ -275,11 +288,17 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
strokeWidth={2}
|
||||
className={iconClassName}
|
||||
style={{ color: 'rgb(160 160 160)' }}
|
||||
onClick={handleFolderCollapse}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="ml-1 flex items-center overflow-hidden">
|
||||
<div
|
||||
className="ml-1 flex items-center overflow-hidden flex-1"
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
<RequestMethod item={item} />
|
||||
<span className="item-name" title={item.name}>
|
||||
{item.name}
|
||||
|
@ -17,7 +17,6 @@ const RenameCollection = ({ collection, onClose }) => {
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
|
@ -68,6 +68,17 @@ const Collection = ({ collection, searchText }) => {
|
||||
dispatch(collectionClicked(collection.uid));
|
||||
};
|
||||
|
||||
const handleCollapseCollection = () => {
|
||||
dispatch(collectionClicked(collection.uid));
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'collection-settings'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const handleRightClick = (event) => {
|
||||
const _menuDropdown = menuDropdownTippyRef.current;
|
||||
if (_menuDropdown) {
|
||||
@ -141,16 +152,17 @@ const Collection = ({ collection, searchText }) => {
|
||||
<div className="flex py-1 collection-name items-center" ref={drop}>
|
||||
<div
|
||||
className="flex flex-grow items-center overflow-hidden"
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
>
|
||||
<IconChevronRight
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
className={iconClassName}
|
||||
style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<div className="ml-1" id="sidebar-collection-name">
|
||||
<div className="ml-1" id="sidebar-collection-name"
|
||||
onClick={handleCollapseCollection}
|
||||
onContextMenu={handleRightClick}>
|
||||
{collection.name}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -184,7 +184,7 @@ const Sidebar = () => {
|
||||
Star
|
||||
</GitHubButton> */}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.34.2</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.36.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -63,16 +63,16 @@ const Table = ({ minColumnWidth = 1, headers = [], children }) => {
|
||||
[activeColumnIndex, columns, minColumnWidth]
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setActiveColumnIndex(null);
|
||||
removeListeners();
|
||||
}, [removeListeners]);
|
||||
|
||||
const removeListeners = useCallback(() => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseup', removeListeners);
|
||||
}, [handleMouseMove]);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setActiveColumnIndex(null);
|
||||
removeListeners?.();
|
||||
}, [removeListeners]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeColumnIndex !== null) {
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
|
14
packages/bruno-app/src/index.js
Normal file
14
packages/bruno-app/src/index.js
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './pages/index';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
if (rootElement) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
@ -31,6 +31,7 @@ if (!SERVER_RENDERED) {
|
||||
require('codemirror/addon/search/jump-to-line');
|
||||
require('codemirror/addon/search/search');
|
||||
require('codemirror/addon/search/searchcursor');
|
||||
require('codemirror/addon/display/placeholder');
|
||||
require('codemirror/keymap/sublime');
|
||||
|
||||
require('codemirror-graphql/hint');
|
||||
|
@ -25,31 +25,7 @@ import '@fontsource/inter/900.css';
|
||||
import { setupPolyfills } from 'utils/common/setupPolyfills';
|
||||
setupPolyfills();
|
||||
|
||||
function SafeHydrate({ children }) {
|
||||
return <div suppressHydrationWarning>{typeof window === 'undefined' ? null : children}</div>;
|
||||
}
|
||||
|
||||
function NoSsr({ children }) {
|
||||
const SERVER_RENDERED = typeof window === 'undefined';
|
||||
|
||||
if (SERVER_RENDERED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
const [domLoaded, setDomLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDomLoaded(true);
|
||||
}, []);
|
||||
|
||||
if (!domLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function Main({ children }) {
|
||||
if (!window.ipcRenderer) {
|
||||
return (
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 mx-10 my-10 rounded relative" role="alert">
|
||||
@ -65,23 +41,21 @@ function MyApp({ Component, pageProps }) {
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<SafeHydrate>
|
||||
<NoSsr>
|
||||
<Provider store={ReduxStore}>
|
||||
<ThemeProvider>
|
||||
<ToastProvider>
|
||||
<AppProvider>
|
||||
<HotkeysProvider>
|
||||
<Component {...pageProps} />
|
||||
</HotkeysProvider>
|
||||
</AppProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</NoSsr>
|
||||
</SafeHydrate>
|
||||
<Provider store={ReduxStore}>
|
||||
<ThemeProvider>
|
||||
<ToastProvider>
|
||||
<AppProvider>
|
||||
<HotkeysProvider>
|
||||
{children}
|
||||
</HotkeysProvider>
|
||||
</AppProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
export default Main;
|
||||
|
||||
|
@ -1,41 +0,0 @@
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||
import { ServerStyleSheet } from 'styled-components';
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
static async getInitialProps(ctx) {
|
||||
const sheet = new ServerStyleSheet();
|
||||
const originalRenderPage = ctx.renderPage;
|
||||
|
||||
try {
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />)
|
||||
});
|
||||
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
return {
|
||||
...initialProps,
|
||||
styles: (
|
||||
<>
|
||||
{initialProps.styles}
|
||||
{sheet.getStyleElement()}
|
||||
</>
|
||||
)
|
||||
};
|
||||
} finally {
|
||||
sheet.seal();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<body id="bruno-app-body">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,20 +1,16 @@
|
||||
import Head from 'next/head';
|
||||
import Bruno from './Bruno';
|
||||
import GlobalStyle from '../globalStyles';
|
||||
import '../i18n';
|
||||
import Main from './Main';
|
||||
|
||||
export default function Home() {
|
||||
export default function App() {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Bruno</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<GlobalStyle />
|
||||
|
||||
<main>
|
||||
<Bruno />
|
||||
<Main>
|
||||
<GlobalStyle />
|
||||
<Bruno />
|
||||
</Main>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
@ -7,21 +7,19 @@
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import getConfig from 'next/config';
|
||||
import { PostHog } from 'posthog-node';
|
||||
import platformLib from 'platform';
|
||||
import { uuid } from 'utils/common';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const posthogApiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY;
|
||||
let posthogClient = null;
|
||||
|
||||
const isPlaywrightTestRunning = () => {
|
||||
return publicRuntimeConfig.PLAYWRIGHT ? true : false;
|
||||
return process.env.PLAYWRIGHT ? true : false;
|
||||
};
|
||||
|
||||
const isDevEnv = () => {
|
||||
return publicRuntimeConfig.ENV === 'dev';
|
||||
return import.meta.env.MODE === 'development';
|
||||
};
|
||||
|
||||
const getPosthogClient = () => {
|
||||
@ -60,7 +58,7 @@ const trackStart = () => {
|
||||
event: 'start',
|
||||
properties: {
|
||||
os: platformLib.os.family,
|
||||
version: '1.34.2'
|
||||
version: '1.36.0'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -6,7 +6,12 @@ import { useSelector, useDispatch } from 'react-redux';
|
||||
import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import { sendRequest, saveRequest, saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import {
|
||||
sendRequest,
|
||||
saveRequest,
|
||||
saveCollectionRoot,
|
||||
saveFolderRoot
|
||||
} from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
||||
import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { getKeyBindingsForActionAllOS } from './keyMappings';
|
||||
@ -43,7 +48,11 @@ export const HotkeysProvider = (props) => {
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, activeTab.uid);
|
||||
if (item && item.uid) {
|
||||
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
|
||||
if (activeTab.type === 'folder-settings') {
|
||||
dispatch(saveFolderRoot(collection.uid, item.uid));
|
||||
} else {
|
||||
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
|
||||
}
|
||||
} else if (activeTab.type === 'collection-settings') {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import getConfig from 'next/config';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import tasksMiddleware from './middlewares/tasks/middleware';
|
||||
import debugMiddleware from './middlewares/debug/middleware';
|
||||
@ -8,9 +7,8 @@ import tabsReducer from './slices/tabs';
|
||||
import notificationsReducer from './slices/notifications';
|
||||
import globalEnvironmentsReducer from './slices/global-environments';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const isDevEnv = () => {
|
||||
return publicRuntimeConfig.ENV === 'dev';
|
||||
return import.meta.env.MODE === 'development';
|
||||
};
|
||||
|
||||
let middleware = [tasksMiddleware.middleware];
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { uuid } from 'utils/common';
|
||||
import path from 'path';
|
||||
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, debounce } from 'lodash';
|
||||
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set } from 'lodash';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import {
|
||||
addDepth,
|
||||
@ -13,6 +12,7 @@ import {
|
||||
findEnvironmentInCollection,
|
||||
findItemInCollection,
|
||||
findItemInCollectionByPathname,
|
||||
isItemAFolder,
|
||||
isItemARequest
|
||||
} from 'utils/collections';
|
||||
import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url';
|
||||
@ -765,6 +765,7 @@ export const collectionsSlice = createSlice({
|
||||
name: '',
|
||||
value: action.payload.value,
|
||||
description: '',
|
||||
contentType: '',
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
@ -786,6 +787,7 @@ export const collectionsSlice = createSlice({
|
||||
param.name = action.payload.param.name;
|
||||
param.value = action.payload.param.value;
|
||||
param.description = action.payload.param.description;
|
||||
param.contentType = action.payload.param.contentType;
|
||||
param.enabled = action.payload.param.enabled;
|
||||
}
|
||||
}
|
||||
@ -1673,6 +1675,9 @@ export const collectionsSlice = createSlice({
|
||||
if (type === 'testrun-ended') {
|
||||
const info = collection.runnerResult.info;
|
||||
info.status = 'ended';
|
||||
if (action.payload.statusText) {
|
||||
info.statusText = action.payload.statusText;
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'request-queued') {
|
||||
@ -1710,6 +1715,12 @@ export const collectionsSlice = createSlice({
|
||||
item.responseReceived = action.payload.responseReceived;
|
||||
item.status = 'error';
|
||||
}
|
||||
|
||||
if (type === 'runner-request-skipped') {
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.status = 'skipped';
|
||||
item.responseReceived = action.payload.responseReceived;
|
||||
}
|
||||
}
|
||||
},
|
||||
resetCollectionRunner: (state, action) => {
|
||||
@ -1733,6 +1744,15 @@ export const collectionsSlice = createSlice({
|
||||
item.draft.request.docs = action.payload.docs;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateFolderDocs: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
if (isItemAFolder(folder)) {
|
||||
set(folder, 'root.docs', action.payload.docs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1827,7 +1847,8 @@ export const {
|
||||
runRequestEvent,
|
||||
runFolderEvent,
|
||||
resetCollectionRunner,
|
||||
updateRequestDocs
|
||||
updateRequestDocs,
|
||||
updateFolderDocs
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
export default collectionsSlice.reducer;
|
||||
|
@ -191,7 +191,7 @@ export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) =>
|
||||
// update existing values
|
||||
variables = variables?.map?.(variable => ({
|
||||
...variable,
|
||||
value: stringifyIfNot(globalEnvironmentVariables?.[variable?.name])
|
||||
value: globalEnvironmentVariables?.[variable?.name]
|
||||
}));
|
||||
|
||||
// add new env values
|
||||
@ -201,7 +201,7 @@ export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) =>
|
||||
variables.push({
|
||||
uid: uuid(),
|
||||
name: key,
|
||||
value: stringifyIfNot(value),
|
||||
value,
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
|
@ -9,6 +9,7 @@ const getReadNotificationIds = () => {
|
||||
return readNotificationIds;
|
||||
} catch (err) {
|
||||
toast.error('An error occurred while fetching read notifications');
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
@ -34,7 +35,6 @@ export const notificationSlice = createSlice({
|
||||
state.loading = action.payload.fetching;
|
||||
},
|
||||
setNotifications: (state, action) => {
|
||||
console.log('notifications', notifications);
|
||||
let notifications = action.payload.notifications || [];
|
||||
let readNotificationIds = state.readNotificationIds;
|
||||
|
||||
@ -58,14 +58,16 @@ export const notificationSlice = createSlice({
|
||||
});
|
||||
},
|
||||
markNotificationAsRead: (state, action) => {
|
||||
if (state.readNotificationIds.includes(action.payload.notificationId)) return;
|
||||
const { notificationId } = action.payload;
|
||||
|
||||
if (state.readNotificationIds.includes(notificationId)) return;
|
||||
|
||||
const notification = state.notifications.find(
|
||||
(notification) => notification.id === action.payload.notificationId
|
||||
(notification) => notification.id === notificationId
|
||||
);
|
||||
if (!notification) return;
|
||||
|
||||
state.readNotificationIds.push(action.payload.notificationId);
|
||||
state.readNotificationIds.push(notificationId);
|
||||
setReadNotificationsIds(state.readNotificationIds);
|
||||
notification.read = true;
|
||||
},
|
||||
|
@ -93,9 +93,6 @@ if (!SERVER_RENDERED) {
|
||||
|
||||
const box = target.getBoundingClientRect();
|
||||
|
||||
const hoverTime = getHoverTime(cm);
|
||||
state.hoverTimeout = setTimeout(onHover, hoverTime);
|
||||
|
||||
const onMouseMove = function () {
|
||||
clearTimeout(state.hoverTimeout);
|
||||
state.hoverTimeout = setTimeout(onHover, hoverTime);
|
||||
@ -115,6 +112,9 @@ if (!SERVER_RENDERED) {
|
||||
onMouseHover(cm, box);
|
||||
};
|
||||
|
||||
const hoverTime = getHoverTime(cm);
|
||||
state.hoverTimeout = setTimeout(onHover, hoverTime);
|
||||
|
||||
CodeMirror.on(document, 'mousemove', onMouseMove);
|
||||
CodeMirror.on(cm.getWrapperElement(), 'mouseout', onMouseOut);
|
||||
}
|
||||
|
@ -303,7 +303,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
script: si.request.script,
|
||||
vars: si.request.vars,
|
||||
assertions: si.request.assertions,
|
||||
tests: si.request.tests
|
||||
tests: si.request.tests,
|
||||
docs: si.request.docs
|
||||
};
|
||||
|
||||
// Handle auth object dynamically
|
||||
@ -403,7 +404,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
request: {}
|
||||
};
|
||||
|
||||
let { request, meta } = si?.root || {};
|
||||
let { request, meta, docs } = si?.root || {};
|
||||
let { headers, script = {}, vars = {}, tests } = request || {};
|
||||
|
||||
// folder level headers
|
||||
@ -435,6 +436,11 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
di.root.request.tests = tests;
|
||||
}
|
||||
|
||||
// folder level docs
|
||||
if (docs?.length) {
|
||||
di.root.docs = docs;
|
||||
}
|
||||
|
||||
if (meta?.name) {
|
||||
di.root.meta = {};
|
||||
di.root.meta.name = meta?.name;
|
||||
@ -791,7 +797,7 @@ export const getGlobalEnvironmentVariables = ({ globalEnvironments, activeGlobal
|
||||
const environment = globalEnvironments?.find(env => env?.uid === activeGlobalEnvironmentUid);
|
||||
if (environment) {
|
||||
each(environment.variables, (variable) => {
|
||||
if (variable.name && variable.value && variable.enabled) {
|
||||
if (variable.name && variable.enabled) {
|
||||
variables[variable.name] = variable.value;
|
||||
}
|
||||
});
|
||||
@ -799,6 +805,19 @@ export const getGlobalEnvironmentVariables = ({ globalEnvironments, activeGlobal
|
||||
return variables;
|
||||
};
|
||||
|
||||
export const getGlobalEnvironmentVariablesMasked = ({ globalEnvironments, activeGlobalEnvironmentUid }) => {
|
||||
const environment = globalEnvironments?.find(env => env?.uid === activeGlobalEnvironmentUid);
|
||||
|
||||
if (environment && Array.isArray(environment.variables)) {
|
||||
return environment.variables
|
||||
.filter((variable) => variable.name && variable.value && variable.enabled && variable.secret)
|
||||
.map((variable) => variable.name);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
|
||||
export const getEnvironmentVariables = (collection) => {
|
||||
let variables = {};
|
||||
if (collection) {
|
||||
@ -872,8 +891,22 @@ export const getAllVariables = (collection, item) => {
|
||||
...requestVariables,
|
||||
...runtimeVariables
|
||||
};
|
||||
const maskedEnvVariables = getEnvironmentVariablesMasked(collection);
|
||||
|
||||
const mergedVariablesGlobal = {
|
||||
...collectionVariables,
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...runtimeVariables,
|
||||
}
|
||||
|
||||
const maskedEnvVariables = getEnvironmentVariablesMasked(collection) || [];
|
||||
const maskedGlobalEnvVariables = collection?.globalEnvSecrets || [];
|
||||
|
||||
const filteredMaskedEnvVariables = maskedEnvVariables.filter((key) => !(key in mergedVariables));
|
||||
const filteredMaskedGlobalEnvVariables = maskedGlobalEnvVariables.filter((key) => !(key in mergedVariablesGlobal));
|
||||
|
||||
const uniqueMaskedVariables = [...new Set([...filteredMaskedEnvVariables, ...filteredMaskedGlobalEnvVariables])];
|
||||
|
||||
return {
|
||||
...globalEnvironmentVariables,
|
||||
@ -885,7 +918,7 @@ export const getAllVariables = (collection, item) => {
|
||||
pathParams: {
|
||||
...pathParams
|
||||
},
|
||||
maskedEnvVariables: filteredMaskedEnvVariables,
|
||||
maskedEnvVariables: uniqueMaskedVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVariables
|
||||
|
@ -151,7 +151,15 @@ export const relativeDate = (dateString) => {
|
||||
export const humanizeDate = (dateString) => {
|
||||
// See this discussion for why .split is necessary
|
||||
// https://stackoverflow.com/questions/7556591/is-the-javascript-date-object-always-one-day-off
|
||||
const date = new Date(dateString.split('-'));
|
||||
|
||||
if (!dateString || typeof dateString !== 'string') {
|
||||
return 'Invalid Date';
|
||||
}
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'Invalid Date';
|
||||
}
|
||||
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
|
@ -58,6 +58,18 @@ describe('common utils', () => {
|
||||
it('should return invalid date if the date is invalid', () => {
|
||||
expect(humanizeDate('9999-99-99')).toBe('Invalid Date');
|
||||
});
|
||||
|
||||
it('should return "Invalid Date" if the date is null', () => {
|
||||
expect(humanizeDate(null)).toBe('Invalid Date');
|
||||
});
|
||||
|
||||
it('should return a humanized date for a valid date in ISO format', () => {
|
||||
expect(humanizeDate('2024-11-28T00:00:00Z')).toBe('November 28, 2024');
|
||||
});
|
||||
|
||||
it('should return "Invalid Date" for a non-date string', () => {
|
||||
expect(humanizeDate('some random text')).toBe('Invalid Date');
|
||||
});
|
||||
});
|
||||
|
||||
describe('relativeDate', () => {
|
||||
|
@ -57,6 +57,8 @@ function getDataString(request) {
|
||||
console.error('Failed to parse JSON data:', error);
|
||||
return { data: request.data.toString() };
|
||||
}
|
||||
} else if (contentType && (contentType.includes('application/xml') || contentType.includes('text/plain'))) {
|
||||
return { data: request.data };
|
||||
}
|
||||
|
||||
const parsedQueryString = querystring.parse(request.data, { sort: false });
|
||||
@ -172,14 +174,14 @@ const curlToJson = (curlCommand) => {
|
||||
}
|
||||
|
||||
if (request.auth) {
|
||||
if(request.auth.mode === 'basic'){
|
||||
if (request.auth.mode === 'basic') {
|
||||
requestJson.auth = {
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username: repr(request.auth.basic?.username),
|
||||
password: repr(request.auth.basic?.password)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque
|
||||
} else if (contentType.includes('application/json')) {
|
||||
body.mode = 'json';
|
||||
body.json = convertToCodeMirrorJson(parsedBody);
|
||||
} else if (contentType.includes('text/xml')) {
|
||||
} else if (contentType.includes('xml')) {
|
||||
body.mode = 'xml';
|
||||
body.xml = parsedBody;
|
||||
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||
|
@ -291,8 +291,9 @@ export const exportCollection = (collection) => {
|
||||
};
|
||||
}
|
||||
default: {
|
||||
console.error('Unsupported auth mode:', itemAuth.mode);
|
||||
return null;
|
||||
return {
|
||||
type: 'noauth'
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -79,9 +79,9 @@ export const transformItemsInCollection = (collection) => {
|
||||
// from 5 feb 2024, multipartFormData needs to have a type
|
||||
// this was introduced when we added support for file uploads
|
||||
// below logic is to make older collection exports backward compatible
|
||||
let multipartFormData = _.get(item, 'request.body.multipartForm');
|
||||
let multipartFormData = get(item, 'request.body.multipartForm');
|
||||
if (multipartFormData) {
|
||||
_.each(multipartFormData, (form) => {
|
||||
each(multipartFormData, (form) => {
|
||||
if (!form.type) {
|
||||
form.type = 'text';
|
||||
}
|
||||
|
@ -166,6 +166,17 @@ const importScriptsFromEvents = (events, requestObject, options, pushTranslation
|
||||
});
|
||||
};
|
||||
|
||||
const importCollectionLevelVariables = (variables, requestObject) => {
|
||||
const vars = variables.map((v) => ({
|
||||
uid: uuid(),
|
||||
name: v.key,
|
||||
value: v.value,
|
||||
enabled: true
|
||||
}));
|
||||
|
||||
requestObject.vars.req = vars;
|
||||
};
|
||||
|
||||
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => {
|
||||
brunoParent.items = brunoParent.items || [];
|
||||
const folderMap = {};
|
||||
@ -188,6 +199,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
|
||||
type: 'folder',
|
||||
items: [],
|
||||
root: {
|
||||
docs: i.description || '',
|
||||
meta: {
|
||||
name: folderName
|
||||
},
|
||||
@ -215,6 +227,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
|
||||
|
||||
brunoParent.items.push(brunoFolderItem);
|
||||
folderMap[folderName] = brunoFolderItem;
|
||||
|
||||
} else {
|
||||
if (i.request) {
|
||||
const baseRequestName = i.name;
|
||||
@ -472,6 +485,7 @@ const importPostmanV2Collection = (collection, options) => {
|
||||
items: [],
|
||||
environments: [],
|
||||
root: {
|
||||
docs: collection.info.description || '',
|
||||
meta: {
|
||||
name: collection.info.name
|
||||
},
|
||||
@ -494,6 +508,10 @@ const importPostmanV2Collection = (collection, options) => {
|
||||
importScriptsFromEvents(collection.event, brunoCollection.root.request, options, pushTranslationLog);
|
||||
}
|
||||
|
||||
if (collection?.variable){
|
||||
importCollectionLevelVariables(collection.variable, brunoCollection.root.request);
|
||||
}
|
||||
|
||||
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth, options);
|
||||
|
||||
return brunoCollection;
|
||||
|
@ -13,6 +13,28 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/usebruno/bruno.git"
|
||||
},
|
||||
"keywords": [
|
||||
"API",
|
||||
"testing",
|
||||
"automation",
|
||||
"cli",
|
||||
"command-line",
|
||||
"bruno",
|
||||
"HTTP requests",
|
||||
"rest-api",
|
||||
"api-client",
|
||||
"api-automation",
|
||||
"request-handling",
|
||||
"mock-api",
|
||||
"http-client",
|
||||
"async",
|
||||
"promise",
|
||||
"javascript",
|
||||
"nodejs",
|
||||
"automation-tool",
|
||||
"postman-alternative",
|
||||
"api-scripting"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "node --experimental-vm-modules $(npx which jest)"
|
||||
},
|
||||
@ -37,7 +59,7 @@
|
||||
"fs-extra": "^10.1.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"json-bigint": "^1.0.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"lodash": "^4.17.21",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
|
@ -1,7 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const chalk = require('chalk');
|
||||
const path = require('path');
|
||||
const { forOwn } = require('lodash');
|
||||
const { forOwn, cloneDeep } = require('lodash');
|
||||
const { exists, isFile, isDirectory } = require('../utils/filesystem');
|
||||
const { runSingleRequest } = require('../runner/run-single-request');
|
||||
const { bruToEnvJson, getEnvVars } = require('../utils/bru');
|
||||
@ -93,8 +93,68 @@ const printRunSummary = (results) => {
|
||||
};
|
||||
};
|
||||
|
||||
const createCollectionFromPath = (collectionPath) => {
|
||||
const environmentsPath = path.join(collectionPath, `environments`);
|
||||
const getFilesInOrder = (collectionPath) => {
|
||||
let collection = {
|
||||
pathname: collectionPath
|
||||
};
|
||||
const traverse = (currentPath) => {
|
||||
const filesInCurrentDir = fs.readdirSync(currentPath);
|
||||
|
||||
if (currentPath.includes('node_modules')) {
|
||||
return;
|
||||
}
|
||||
const currentDirItems = [];
|
||||
for (const file of filesInCurrentDir) {
|
||||
const filePath = path.join(currentPath, file);
|
||||
const stats = fs.lstatSync(filePath);
|
||||
if (
|
||||
stats.isDirectory() &&
|
||||
filePath !== environmentsPath &&
|
||||
!filePath.startsWith('.git') &&
|
||||
!filePath.startsWith('node_modules')
|
||||
) {
|
||||
let folderItem = { name: file, pathname: filePath, type: 'folder', items: traverse(filePath) }
|
||||
const folderBruFilePath = path.join(filePath, 'folder.bru');
|
||||
const folderBruFileExists = fs.existsSync(folderBruFilePath);
|
||||
if(folderBruFileExists) {
|
||||
const folderBruContent = fs.readFileSync(folderBruFilePath, 'utf8');
|
||||
let folderBruJson = collectionBruToJson(folderBruContent);
|
||||
folderItem.root = folderBruJson;
|
||||
}
|
||||
currentDirItems.push(folderItem);
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of filesInCurrentDir) {
|
||||
if (['collection.bru', 'folder.bru'].includes(file)) {
|
||||
continue;
|
||||
}
|
||||
const filePath = path.join(currentPath, file);
|
||||
const stats = fs.lstatSync(filePath);
|
||||
|
||||
if (!stats.isDirectory() && path.extname(filePath) === '.bru') {
|
||||
const bruContent = fs.readFileSync(filePath, 'utf8');
|
||||
const bruJson = bruToJson(bruContent);
|
||||
currentDirItems.push({
|
||||
name: file,
|
||||
pathname: filePath,
|
||||
...bruJson
|
||||
});
|
||||
}
|
||||
}
|
||||
return currentDirItems
|
||||
};
|
||||
collection.items = traverse(collectionPath);
|
||||
return collection;
|
||||
};
|
||||
return getFilesInOrder(collectionPath);
|
||||
};
|
||||
|
||||
const getBruFilesRecursively = (dir, testsOnly) => {
|
||||
const environmentsPath = 'environments';
|
||||
const collection = {};
|
||||
|
||||
const getFilesInOrder = (dir) => {
|
||||
let bruJsons = [];
|
||||
@ -359,6 +419,12 @@ const handler = async function (argv) {
|
||||
const brunoConfigFile = fs.readFileSync(brunoJsonPath, 'utf8');
|
||||
const brunoConfig = JSON.parse(brunoConfigFile);
|
||||
const collectionRoot = getCollectionRoot(collectionPath);
|
||||
let collection = createCollectionFromPath(collectionPath);
|
||||
collection = {
|
||||
brunoConfig,
|
||||
root: collectionRoot,
|
||||
...collection
|
||||
}
|
||||
|
||||
if (clientCertConfig) {
|
||||
try {
|
||||
@ -571,7 +637,7 @@ const handler = async function (argv) {
|
||||
let currentRequestIndex = 0;
|
||||
let nJumps = 0; // count the number of jumps to avoid infinite loops
|
||||
while (currentRequestIndex < bruJsons.length) {
|
||||
const iter = bruJsons[currentRequestIndex];
|
||||
const iter = cloneDeep(bruJsons[currentRequestIndex]);
|
||||
const { bruFilepath, bruJson } = iter;
|
||||
|
||||
const start = process.hrtime();
|
||||
@ -584,7 +650,8 @@ const handler = async function (argv) {
|
||||
processEnvVars,
|
||||
brunoConfig,
|
||||
collectionRoot,
|
||||
runtime
|
||||
runtime,
|
||||
collection
|
||||
);
|
||||
|
||||
results.push({
|
||||
|
@ -24,7 +24,7 @@ const EXIT_STATUS = {
|
||||
// Invalid output format requested
|
||||
ERROR_INCORRECT_OUTPUT_FORMAT: 9,
|
||||
// Everything else
|
||||
ERROR_GENERIC: 255,
|
||||
ERROR_GENERIC: 255
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
@ -13,14 +13,17 @@ const getContentType = (headers = {}) => {
|
||||
return contentType;
|
||||
};
|
||||
|
||||
const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEnvVars = {}) => {
|
||||
const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}) => {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
// we clone envVars because we don't want to modify the original object
|
||||
envVars = cloneDeep(envVars);
|
||||
envVariables = cloneDeep(envVariables);
|
||||
|
||||
// envVars can inturn have values as {{process.env.VAR_NAME}}
|
||||
// so we need to interpolate envVars first with processEnvVars
|
||||
forOwn(envVars, (value, key) => {
|
||||
envVars[key] = interpolate(value, {
|
||||
forOwn(envVariables, (value, key) => {
|
||||
envVariables[key] = interpolate(value, {
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
@ -36,7 +39,10 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
|
||||
|
||||
// runtimeVariables take precedence over envVars
|
||||
const combinedVars = {
|
||||
...envVars,
|
||||
...collectionVariables,
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
env: {
|
||||
@ -80,11 +86,12 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
|
||||
} catch (err) {}
|
||||
}
|
||||
} else if (contentType === 'multipart/form-data') {
|
||||
if (typeof request.data === 'object' && !(request?.data instanceof FormData)) {
|
||||
if (Array.isArray(request?.data) && !(request.data instanceof FormData)) {
|
||||
try {
|
||||
forOwn(request?.data, (value, key) => {
|
||||
request.data[key] = _interpolate(value);
|
||||
});
|
||||
request.data = request?.data?.map(d => ({
|
||||
...d,
|
||||
value: _interpolate(d?.value)
|
||||
}));
|
||||
} catch (err) {}
|
||||
}
|
||||
} else {
|
||||
|
@ -1,24 +1,24 @@
|
||||
const { get, each, filter } = require('lodash');
|
||||
const fs = require('fs');
|
||||
var JSONbig = require('json-bigint');
|
||||
const decomment = require('decomment');
|
||||
const crypto = require('node:crypto');
|
||||
const { mergeHeaders, mergeScripts, mergeVars, getTreePathFromCollectionToItem } = require('../utils/collection');
|
||||
const { createFormData } = require('../utils/form-data');
|
||||
|
||||
const prepareRequest = (request, collectionRoot) => {
|
||||
const prepareRequest = (item = {}, collection = {}) => {
|
||||
const request = item?.request;
|
||||
const brunoConfig = get(collection, 'brunoConfig', {});
|
||||
const headers = {};
|
||||
let contentTypeDefined = false;
|
||||
|
||||
// collection headers
|
||||
each(get(collectionRoot, 'request.headers', []), (h) => {
|
||||
if (h.enabled) {
|
||||
headers[h.name] = h.value;
|
||||
if (h.name.toLowerCase() === 'content-type') {
|
||||
contentTypeDefined = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
const scriptFlow = brunoConfig?.scripts?.flow ?? 'sandwich';
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
if (requestTreePath && requestTreePath.length > 0) {
|
||||
mergeHeaders(collection, request, requestTreePath);
|
||||
mergeScripts(collection, request, requestTreePath, scriptFlow);
|
||||
mergeVars(collection, request, requestTreePath);
|
||||
}
|
||||
|
||||
each(request.headers, (h) => {
|
||||
each(get(request, 'headers', []), (h) => {
|
||||
if (h.enabled) {
|
||||
headers[h.name] = h.value;
|
||||
if (h.name.toLowerCase() === 'content-type') {
|
||||
@ -31,10 +31,11 @@ const prepareRequest = (request, collectionRoot) => {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: headers,
|
||||
pathParams: request?.params?.filter((param) => param.type === 'path')
|
||||
pathParams: request?.params?.filter((param) => param.type === 'path'),
|
||||
responseType: 'arraybuffer'
|
||||
};
|
||||
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
if (collectionAuth && request.auth.mode === 'inherit') {
|
||||
if (collectionAuth.mode === 'basic') {
|
||||
axiosRequest.auth = {
|
||||
@ -96,16 +97,10 @@ const prepareRequest = (request, collectionRoot) => {
|
||||
if (!contentTypeDefined) {
|
||||
axiosRequest.headers['content-type'] = 'application/json';
|
||||
}
|
||||
let jsonBody;
|
||||
try {
|
||||
jsonBody = decomment(request?.body?.json);
|
||||
axiosRequest.data = decomment(request?.body?.json);
|
||||
} catch (error) {
|
||||
jsonBody = request?.body?.json;
|
||||
}
|
||||
try {
|
||||
axiosRequest.data = JSONbig.parse(jsonBody);
|
||||
} catch (error) {
|
||||
axiosRequest.data = jsonBody;
|
||||
axiosRequest.data = request?.body?.json;
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,7 +113,7 @@ const prepareRequest = (request, collectionRoot) => {
|
||||
|
||||
if (request.body.mode === 'xml') {
|
||||
if (!contentTypeDefined) {
|
||||
axiosRequest.headers['content-type'] = 'text/xml';
|
||||
axiosRequest.headers['content-type'] = 'application/xml';
|
||||
}
|
||||
axiosRequest.data = request.body.xml;
|
||||
}
|
||||
@ -137,13 +132,11 @@ const prepareRequest = (request, collectionRoot) => {
|
||||
each(enabledParams, (p) => (params[p.name] = p.value));
|
||||
axiosRequest.data = params;
|
||||
}
|
||||
|
||||
|
||||
if (request.body.mode === 'multipartForm') {
|
||||
axiosRequest.headers['content-type'] = 'multipart/form-data';
|
||||
const params = {};
|
||||
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
|
||||
each(enabledParams, (p) => (params[p.name] = p.value));
|
||||
axiosRequest.data = params;
|
||||
axiosRequest.data = enabledParams;
|
||||
}
|
||||
|
||||
if (request.body.mode === 'graphql') {
|
||||
@ -157,10 +150,19 @@ const prepareRequest = (request, collectionRoot) => {
|
||||
axiosRequest.data = graphqlQuery;
|
||||
}
|
||||
|
||||
if (request.script && request.script.length) {
|
||||
if (request.script) {
|
||||
axiosRequest.script = request.script;
|
||||
}
|
||||
|
||||
if (request.tests) {
|
||||
axiosRequest.tests = request.tests;
|
||||
}
|
||||
|
||||
axiosRequest.vars = request.vars;
|
||||
axiosRequest.collectionVariables = request.collectionVariables;
|
||||
axiosRequest.folderVariables = request.folderVariables;
|
||||
axiosRequest.requestVariables = request.requestVariables;
|
||||
|
||||
return axiosRequest;
|
||||
};
|
||||
|
||||
|
@ -17,10 +17,11 @@ const { HttpProxyAgent } = require('http-proxy-agent');
|
||||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||
const { makeAxiosInstance } = require('../utils/axios-instance');
|
||||
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
|
||||
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
|
||||
const { shouldUseProxy, PatchedHttpsProxyAgent, getSystemProxyEnvVariables } = require('../utils/proxy-util');
|
||||
const path = require('path');
|
||||
const { createFormData } = require('../utils/common');
|
||||
const { parseDataFromResponse } = require('../utils/common');
|
||||
const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies');
|
||||
const { createFormData } = require('../utils/form-data');
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
|
||||
const onConsoleLog = (type, args) => {
|
||||
@ -36,13 +37,17 @@ const runSingleRequest = async function (
|
||||
processEnvVars,
|
||||
brunoConfig,
|
||||
collectionRoot,
|
||||
runtime
|
||||
runtime,
|
||||
collection
|
||||
) {
|
||||
try {
|
||||
let request;
|
||||
let nextRequestName;
|
||||
|
||||
request = prepareRequest(bruJson.request, collectionRoot);
|
||||
let item = {
|
||||
pathname: path.join(collectionPath, filename),
|
||||
...bruJson
|
||||
}
|
||||
request = prepareRequest(item, collection);
|
||||
|
||||
request.__bruno__executionMode = 'cli';
|
||||
|
||||
@ -50,10 +55,7 @@ const runSingleRequest = async function (
|
||||
scriptingConfig.runtime = runtime;
|
||||
|
||||
// run pre request script
|
||||
const requestScriptFile = compact([
|
||||
get(collectionRoot, 'request.script.req'),
|
||||
get(bruJson, 'request.script.req')
|
||||
]).join(os.EOL);
|
||||
const requestScriptFile = get(request, 'script.req');
|
||||
if (requestScriptFile?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const result = await scriptRuntime.runRequestScript(
|
||||
@ -139,39 +141,85 @@ const runSingleRequest = async function (
|
||||
}
|
||||
}
|
||||
|
||||
// set proxy if enabled
|
||||
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
|
||||
const shouldProxy = shouldUseProxy(request.url, get(brunoConfig, 'proxy.bypassProxy', ''));
|
||||
if (proxyEnabled && shouldProxy) {
|
||||
const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
|
||||
const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
|
||||
const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
|
||||
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
|
||||
const socksEnabled = proxyProtocol.includes('socks');
|
||||
let proxyMode = 'off';
|
||||
let proxyConfig = {};
|
||||
|
||||
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
|
||||
let proxyUri;
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
|
||||
const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
|
||||
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
|
||||
} else {
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
|
||||
const collectionProxyConfig = get(brunoConfig, 'proxy', {});
|
||||
const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', false);
|
||||
if (collectionProxyEnabled === true) {
|
||||
proxyConfig = collectionProxyConfig;
|
||||
proxyMode = 'on';
|
||||
} else {
|
||||
// if the collection level proxy is not set, pick the system level proxy by default, to maintain backward compatibility
|
||||
const { http_proxy, https_proxy } = getSystemProxyEnvVariables();
|
||||
if (http_proxy?.length || https_proxy?.length) {
|
||||
proxyMode = 'system';
|
||||
}
|
||||
}
|
||||
|
||||
if (socksEnabled) {
|
||||
request.httpsAgent = new SocksProxyAgent(
|
||||
proxyUri,
|
||||
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
||||
);
|
||||
request.httpAgent = new SocksProxyAgent(proxyUri);
|
||||
if (proxyMode === 'on') {
|
||||
const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', ''));
|
||||
if (shouldProxy) {
|
||||
const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);
|
||||
const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions);
|
||||
const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions);
|
||||
const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false);
|
||||
const socksEnabled = proxyProtocol.includes('socks');
|
||||
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
|
||||
let proxyUri;
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
|
||||
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
|
||||
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
|
||||
} else {
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
|
||||
}
|
||||
if (socksEnabled) {
|
||||
request.httpsAgent = new SocksProxyAgent(
|
||||
proxyUri,
|
||||
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
||||
);
|
||||
request.httpAgent = new SocksProxyAgent(proxyUri);
|
||||
} else {
|
||||
request.httpsAgent = new PatchedHttpsProxyAgent(
|
||||
proxyUri,
|
||||
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
||||
);
|
||||
request.httpAgent = new HttpProxyAgent(proxyUri);
|
||||
}
|
||||
} else {
|
||||
request.httpsAgent = new PatchedHttpsProxyAgent(
|
||||
proxyUri,
|
||||
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
||||
);
|
||||
request.httpAgent = new HttpProxyAgent(proxyUri);
|
||||
request.httpsAgent = new https.Agent({
|
||||
...httpsAgentRequestFields
|
||||
});
|
||||
}
|
||||
} else if (proxyMode === 'system') {
|
||||
const { http_proxy, https_proxy, no_proxy } = getSystemProxyEnvVariables();
|
||||
const shouldUseSystemProxy = shouldUseProxy(request.url, no_proxy || '');
|
||||
if (shouldUseSystemProxy) {
|
||||
try {
|
||||
if (http_proxy?.length) {
|
||||
new URL(http_proxy);
|
||||
request.httpAgent = new HttpProxyAgent(http_proxy);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('Invalid system http_proxy');
|
||||
}
|
||||
try {
|
||||
if (https_proxy?.length) {
|
||||
new URL(https_proxy);
|
||||
request.httpsAgent = new PatchedHttpsProxyAgent(
|
||||
https_proxy,
|
||||
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('Invalid system https_proxy');
|
||||
}
|
||||
} else {
|
||||
request.httpsAgent = new https.Agent({
|
||||
...httpsAgentRequestFields
|
||||
});
|
||||
}
|
||||
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
@ -226,6 +274,9 @@ const runSingleRequest = async function (
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
response = await axiosInstance(request);
|
||||
|
||||
const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
|
||||
response.data = data;
|
||||
|
||||
// Prevents the duration on leaking to the actual result
|
||||
responseTime = response.headers.get('request-duration');
|
||||
response.headers.delete('request-duration');
|
||||
@ -236,6 +287,8 @@ const runSingleRequest = async function (
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.response) {
|
||||
const { data } = parseDataFromResponse(err?.response);
|
||||
err.response.data = data;
|
||||
response = err.response;
|
||||
|
||||
// Prevents the duration on leaking to the actual result
|
||||
@ -291,10 +344,7 @@ const runSingleRequest = async function (
|
||||
}
|
||||
|
||||
// run post response script
|
||||
const responseScriptFile = compact([
|
||||
get(collectionRoot, 'request.script.res'),
|
||||
get(bruJson, 'request.script.res')
|
||||
]).join(os.EOL);
|
||||
const responseScriptFile = get(request, 'script.res');
|
||||
if (responseScriptFile?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const result = await scriptRuntime.runResponseScript(
|
||||
@ -339,7 +389,7 @@ const runSingleRequest = async function (
|
||||
|
||||
// run tests
|
||||
let testResults = [];
|
||||
const testFile = compact([get(collectionRoot, 'request.tests'), get(bruJson, 'request.tests')]).join(os.EOL);
|
||||
const testFile = get(request, 'tests');
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const result = await testRuntime.runTests(
|
||||
|
@ -10,6 +10,7 @@ const { CLI_VERSION } = require('../constants');
|
||||
function makeAxiosInstance() {
|
||||
/** @type {axios.AxiosInstance} */
|
||||
const instance = axios.create({
|
||||
proxy: false,
|
||||
headers: {
|
||||
"User-Agent": `bruno-runtime/${CLI_VERSION}`
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ const bruToJson = (bru) => {
|
||||
body: _.get(json, 'body', {}),
|
||||
vars: _.get(json, 'vars', []),
|
||||
assertions: _.get(json, 'assertions', []),
|
||||
script: _.get(json, 'script', ''),
|
||||
script: _.get(json, 'script', {}),
|
||||
tests: _.get(json, 'tests', '')
|
||||
}
|
||||
};
|
||||
|
208
packages/bruno-cli/src/utils/collection.js
Normal file
208
packages/bruno-cli/src/utils/collection.js
Normal file
@ -0,0 +1,208 @@
|
||||
const { get, each, find, compact } = require('lodash');
|
||||
const os = require('os');
|
||||
|
||||
const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let headers = new Map();
|
||||
|
||||
let collectionHeaders = get(collection, 'root.request.headers', []);
|
||||
collectionHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header.value);
|
||||
}
|
||||
});
|
||||
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let _headers = get(i, 'root.request.headers', []);
|
||||
_headers.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
|
||||
_headers.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
request.headers = Array.from(headers, ([name, value]) => ({ name, value, enabled: true }));
|
||||
};
|
||||
|
||||
const mergeVars = (collection, request, requestTreePath) => {
|
||||
let reqVars = new Map();
|
||||
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
|
||||
let collectionVariables = {};
|
||||
collectionRequestVars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
reqVars.set(_var.name, _var.value);
|
||||
collectionVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
let folderVariables = {};
|
||||
let requestVariables = {};
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
reqVars.set(_var.name, _var.value);
|
||||
folderVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
reqVars.set(_var.name, _var.value);
|
||||
requestVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
request.collectionVariables = collectionVariables;
|
||||
request.folderVariables = folderVariables;
|
||||
request.requestVariables = requestVariables;
|
||||
|
||||
if(request?.vars) {
|
||||
request.vars.req = Array.from(reqVars, ([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
enabled: true,
|
||||
type: 'request'
|
||||
}));
|
||||
}
|
||||
|
||||
let resVars = new Map();
|
||||
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
|
||||
collectionResponseVars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
resVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.res', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
resVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
resVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(request?.vars) {
|
||||
request.vars.res = Array.from(resVars, ([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
enabled: true,
|
||||
type: 'response'
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
let collectionPreReqScript = get(collection, 'root.request.script.req', '');
|
||||
let collectionPostResScript = get(collection, 'root.request.script.res', '');
|
||||
let collectionTests = get(collection, 'root.request.tests', '');
|
||||
|
||||
let combinedPreReqScript = [];
|
||||
let combinedPostResScript = [];
|
||||
let combinedTests = [];
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let preReqScript = get(i, 'root.request.script.req', '');
|
||||
if (preReqScript && preReqScript.trim() !== '') {
|
||||
combinedPreReqScript.push(preReqScript);
|
||||
}
|
||||
|
||||
let postResScript = get(i, 'root.request.script.res', '');
|
||||
if (postResScript && postResScript.trim() !== '') {
|
||||
combinedPostResScript.push(postResScript);
|
||||
}
|
||||
|
||||
let tests = get(i, 'root.request.tests', '');
|
||||
if (tests && tests?.trim?.() !== '') {
|
||||
combinedTests.push(tests);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request.script.req = compact([collectionPreReqScript, ...combinedPreReqScript, request?.script?.req || '']).join(os.EOL);
|
||||
|
||||
if (scriptFlow === 'sequential') {
|
||||
request.script.res = compact([collectionPostResScript, ...combinedPostResScript, request?.script?.res || '']).join(os.EOL);
|
||||
} else {
|
||||
request.script.res = compact([request?.script?.res || '', ...combinedPostResScript.reverse(), collectionPostResScript]).join(os.EOL);
|
||||
}
|
||||
|
||||
if (scriptFlow === 'sequential') {
|
||||
request.tests = compact([collectionTests, ...combinedTests, request?.tests || '']).join(os.EOL);
|
||||
} else {
|
||||
request.tests = compact([request?.tests || '', ...combinedTests.reverse(), collectionTests]).join(os.EOL);
|
||||
}
|
||||
};
|
||||
|
||||
const findItem = (items = [], pathname) => {
|
||||
return find(items, (i) => i.pathname === pathname);
|
||||
};
|
||||
|
||||
const findItemInCollection = (collection, pathname) => {
|
||||
let flattenedItems = flattenItems(collection.items);
|
||||
|
||||
return findItem(flattenedItems, pathname);
|
||||
};
|
||||
|
||||
const findParentItemInCollection = (collection, pathname) => {
|
||||
let flattenedItems = flattenItems(collection.items);
|
||||
|
||||
return find(flattenedItems, (item) => {
|
||||
return item.items && find(item.items, (i) => i.pathname === pathname);
|
||||
});
|
||||
};
|
||||
|
||||
const flattenItems = (items = []) => {
|
||||
const flattenedItems = [];
|
||||
|
||||
const flatten = (itms, flattened) => {
|
||||
each(itms, (i) => {
|
||||
flattened.push(i);
|
||||
|
||||
if (i.items && i.items.length) {
|
||||
flatten(i.items, flattened);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
flatten(items, flattenedItems);
|
||||
|
||||
return flattenedItems;
|
||||
};
|
||||
|
||||
const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _item.pathname);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item.pathname);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
mergeHeaders,
|
||||
mergeVars,
|
||||
mergeScripts,
|
||||
getTreePathFromCollectionToItem
|
||||
}
|
@ -1,7 +1,4 @@
|
||||
const fs = require('fs');
|
||||
const FormData = require('form-data');
|
||||
const { forOwn } = require('lodash');
|
||||
const path = require('path');
|
||||
const iconv = require('iconv-lite');
|
||||
|
||||
const lpad = (str, width) => {
|
||||
let paddedStr = str;
|
||||
@ -19,33 +16,38 @@ const rpad = (str, width) => {
|
||||
return paddedStr;
|
||||
};
|
||||
|
||||
const createFormData = (datas, collectionPath) => {
|
||||
// make axios work in node using form data
|
||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||
const form = new FormData();
|
||||
forOwn(datas, (value, key) => {
|
||||
if (typeof value == 'string') {
|
||||
form.append(key, value);
|
||||
return;
|
||||
const parseDataFromResponse = (response, disableParsingResponseJson = false) => {
|
||||
// Parse the charset from content type: https://stackoverflow.com/a/33192813
|
||||
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || '');
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals
|
||||
const charsetValue = charsetMatch?.[1];
|
||||
const dataBuffer = Buffer.from(response.data);
|
||||
// Overwrite the original data for backwards compatibility
|
||||
let data;
|
||||
if (iconv.encodingExists(charsetValue)) {
|
||||
data = iconv.decode(dataBuffer, charsetValue);
|
||||
} else {
|
||||
data = iconv.decode(dataBuffer, 'utf-8');
|
||||
}
|
||||
// Try to parse response to JSON, this can quietly fail
|
||||
try {
|
||||
// Filter out ZWNBSP character
|
||||
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
|
||||
data = data.replace(/^\uFEFF/, '');
|
||||
|
||||
// If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed
|
||||
if (!disableParsingResponseJson && !(typeof data === 'string' && data.startsWith('"') && data.endsWith('"'))) {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
} catch {
|
||||
|
||||
const filePaths = value || [];
|
||||
filePaths?.forEach?.((filePath) => {
|
||||
let trimmedFilePath = filePath.trim();
|
||||
}
|
||||
|
||||
if (!path.isAbsolute(trimmedFilePath)) {
|
||||
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
|
||||
}
|
||||
|
||||
form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
|
||||
});
|
||||
});
|
||||
return form;
|
||||
return { data, dataBuffer };
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
lpad,
|
||||
rpad,
|
||||
createFormData
|
||||
parseDataFromResponse
|
||||
};
|
||||
|
42
packages/bruno-cli/src/utils/form-data.js
Normal file
42
packages/bruno-cli/src/utils/form-data.js
Normal file
@ -0,0 +1,42 @@
|
||||
const { forEach } = require('lodash');
|
||||
const FormData = require('form-data');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const createFormData = (data, collectionPath) => {
|
||||
// make axios work in node using form data
|
||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||
const form = new FormData();
|
||||
forEach(data, (datum) => {
|
||||
const { name, type, value, contentType } = datum;
|
||||
let options = {};
|
||||
if (contentType) {
|
||||
options.contentType = contentType;
|
||||
}
|
||||
if (type === 'text') {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => form.append(name, val, options));
|
||||
} else {
|
||||
form.append(name, value, options);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'file') {
|
||||
const filePaths = value || [];
|
||||
filePaths.forEach((filePath) => {
|
||||
let trimmedFilePath = filePath.trim();
|
||||
if (!path.isAbsolute(trimmedFilePath)) {
|
||||
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
|
||||
}
|
||||
options.filename = path.basename(trimmedFilePath);
|
||||
form.append(name, fs.createReadStream(trimmedFilePath), options);
|
||||
});
|
||||
}
|
||||
});
|
||||
return form;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createFormData
|
||||
}
|
@ -79,7 +79,18 @@ class PatchedHttpsProxyAgent extends HttpsProxyAgent {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getSystemProxyEnvVariables = () => {
|
||||
const { http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, no_proxy, NO_PROXY } = process.env;
|
||||
return {
|
||||
http_proxy: http_proxy || HTTP_PROXY,
|
||||
https_proxy: https_proxy || HTTPS_PROXY,
|
||||
no_proxy: no_proxy || NO_PROXY
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldUseProxy,
|
||||
PatchedHttpsProxyAgent
|
||||
PatchedHttpsProxyAgent,
|
||||
getSystemProxyEnvVariables
|
||||
};
|
||||
|
@ -6,15 +6,19 @@ describe('prepare-request: prepareRequest', () => {
|
||||
describe('Decomments request body', () => {
|
||||
it('If request body is valid JSON', async () => {
|
||||
const body = { mode: 'json', json: '{\n"test": "{{someVar}}" // comment\n}' };
|
||||
const expected = { test: '{{someVar}}' };
|
||||
const result = prepareRequest({ body });
|
||||
const expected = `{
|
||||
\"test\": \"{{someVar}}\"
|
||||
}`;
|
||||
const result = prepareRequest({ request: { body } });
|
||||
expect(result.data).toEqual(expected);
|
||||
});
|
||||
|
||||
it('If request body is not valid JSON', async () => {
|
||||
const body = { mode: 'json', json: '{\n"test": {{someVar}} // comment\n}' };
|
||||
const expected = '{\n"test": {{someVar}} \n}';
|
||||
const result = prepareRequest({ body });
|
||||
const expected = `{
|
||||
\"test\": {{someVar}}
|
||||
}`;
|
||||
const result = prepareRequest({ request: { body } });
|
||||
expect(result.data).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
@ -3,7 +3,7 @@ require('dotenv').config({ path: process.env.DOTENV_PATH });
|
||||
const config = {
|
||||
appId: 'com.usebruno.app',
|
||||
productName: 'Bruno',
|
||||
electronVersion: '31.2.1',
|
||||
electronVersion: '33.2.1',
|
||||
directories: {
|
||||
buildResources: 'resources',
|
||||
output: 'out'
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "v1.34.2",
|
||||
"version": "v1.36.0",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
@ -50,7 +50,7 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "3.3.4",
|
||||
"nanoid": "3.3.8",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
@ -62,7 +62,8 @@
|
||||
"dmg-license": "^1.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "31.2.1",
|
||||
"electron-builder": "25.1.8"
|
||||
"electron": "33.2.1",
|
||||
"electron-builder": "25.1.8",
|
||||
"electron-devtools-installer": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -389,6 +389,8 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
|
||||
};
|
||||
|
||||
const unlink = (win, pathname, collectionUid, collectionPath) => {
|
||||
console.log(`watcher unlink: ${pathname}`);
|
||||
|
||||
if (isBruEnvironmentConfig(pathname, collectionPath)) {
|
||||
return unlinkEnvironmentFile(win, pathname, collectionUid);
|
||||
}
|
||||
@ -506,6 +508,33 @@ class Watcher {
|
||||
this.watchers[watchPath] = null;
|
||||
}
|
||||
}
|
||||
|
||||
getWatcherByItemPath(itemPath) {
|
||||
const paths = Object.keys(this.watchers);
|
||||
|
||||
const watcherPath = paths?.find(collectionPath => {
|
||||
const absCollectionPath = path.resolve(collectionPath);
|
||||
const absItemPath = path.resolve(itemPath);
|
||||
|
||||
return absItemPath.startsWith(absCollectionPath);
|
||||
});
|
||||
|
||||
return watcherPath ? this.watchers[watcherPath] : null;
|
||||
}
|
||||
|
||||
unlinkItemPathInWatcher(itemPath) {
|
||||
const watcher = this.getWatcherByItemPath(itemPath);
|
||||
if (watcher) {
|
||||
watcher.unwatch(itemPath);
|
||||
}
|
||||
}
|
||||
|
||||
addItemPathInWatcher(itemPath) {
|
||||
const watcher = this.getWatcherByItemPath(itemPath);
|
||||
if (watcher && !watcher?.has?.(itemPath)) {
|
||||
watcher?.add?.(itemPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Watcher;
|
||||
|
@ -11,7 +11,7 @@ if (isDev) {
|
||||
}
|
||||
|
||||
const { format } = require('url');
|
||||
const { BrowserWindow, app, Menu, ipcMain } = require('electron');
|
||||
const { BrowserWindow, app, session, Menu, ipcMain } = require('electron');
|
||||
const { setContentSecurityPolicy } = require('electron-util');
|
||||
|
||||
const menuTemplate = require('./app/menu-template');
|
||||
@ -51,6 +51,24 @@ let watcher;
|
||||
|
||||
// Prepare the renderer once the app is ready
|
||||
app.on('ready', async () => {
|
||||
|
||||
if (isDev) {
|
||||
const { installExtension, REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer');
|
||||
try {
|
||||
const extensions = await installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS], {
|
||||
loadExtensionOptions: {allowFileAccess: true},
|
||||
})
|
||||
console.log(`Added Extensions: ${extensions.map(ext => ext.name).join(", ")}`)
|
||||
await require("node:timers/promises").setTimeout(1000);
|
||||
session.defaultSession.getAllExtensions().map((ext) => {
|
||||
console.log(`Loading Extension: ${ext.name}`);
|
||||
session.defaultSession.loadExtension(ext.path)
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('An error occurred while loading extensions: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
Menu.setApplicationMenu(menu);
|
||||
const { maximized, x, y, width, height } = loadWindowState();
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const fsExtra = require('fs-extra');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { ipcMain, shell, dialog, app } = require('electron');
|
||||
const { envJsonToBru, bruToJson, jsonToBru, jsonToCollectionBru } = require('../bru');
|
||||
@ -17,7 +19,11 @@ const {
|
||||
isWSLPath,
|
||||
normalizeWslPath,
|
||||
normalizeAndResolvePath,
|
||||
safeToRename
|
||||
safeToRename,
|
||||
sanitizeCollectionName,
|
||||
isWindowsOS,
|
||||
isValidFilename,
|
||||
hasSubDirectories,
|
||||
} = require('../utils/filesystem');
|
||||
const { openCollectionDialog } = require('../app/collections');
|
||||
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
|
||||
@ -63,6 +69,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
'renderer:create-collection',
|
||||
async (event, collectionName, collectionFolderName, collectionLocation) => {
|
||||
try {
|
||||
collectionFolderName = sanitizeDirectoryName(collectionFolderName);
|
||||
collectionName = sanitizeCollectionName(collectionName);
|
||||
const dirPath = path.join(collectionLocation, collectionFolderName);
|
||||
if (fs.existsSync(dirPath)) {
|
||||
const files = fs.readdirSync(dirPath);
|
||||
@ -71,9 +79,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
throw new Error(`collection: ${dirPath} already exists and is not empty`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValidPathname(dirPath)) {
|
||||
throw new Error(`collection: invalid pathname - ${dir}`);
|
||||
if (!isValidPathname(path.basename(dirPath))) {
|
||||
throw new Error(`collection: invalid pathname - ${dirPath}`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
@ -101,13 +108,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
ipcMain.handle(
|
||||
'renderer:clone-collection',
|
||||
async (event, collectionName, collectionFolderName, collectionLocation, previousPath) => {
|
||||
collectionFolderName = sanitizeCollectionName(collectionFolderName);
|
||||
const dirPath = path.join(collectionLocation, collectionFolderName);
|
||||
if (fs.existsSync(dirPath)) {
|
||||
throw new Error(`collection: ${dirPath} already exists`);
|
||||
}
|
||||
|
||||
if (!isValidPathname(dirPath)) {
|
||||
throw new Error(`collection: invalid pathname - ${dir}`);
|
||||
if (!isValidPathname(path.basename(dirPath))) {
|
||||
throw new Error(`collection: invalid pathname - ${dirPath}`);
|
||||
}
|
||||
|
||||
// create dir
|
||||
@ -146,6 +154,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
// rename collection
|
||||
ipcMain.handle('renderer:rename-collection', async (event, newName, collectionPathname) => {
|
||||
try {
|
||||
newName = sanitizeCollectionName(newName);
|
||||
const brunoJsonFilePath = path.join(collectionPathname, 'bruno.json');
|
||||
const content = fs.readFileSync(brunoJsonFilePath, 'utf8');
|
||||
const json = JSON.parse(content);
|
||||
@ -201,7 +210,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
if (fs.existsSync(pathname)) {
|
||||
throw new Error(`path: ${pathname} already exists`);
|
||||
}
|
||||
|
||||
if (!isValidFilename(request.name)) {
|
||||
throw new Error(`path: ${request.name}.bru is not a valid filename`);
|
||||
}
|
||||
const content = jsonToBru(request);
|
||||
await writeFile(pathname, content);
|
||||
} catch (error) {
|
||||
@ -337,6 +348,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
// rename item
|
||||
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
|
||||
const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`);
|
||||
// const parentDir = path.dirname(oldPath);
|
||||
const isWindowsOSAndNotWSLAndItemHasSubDirectories = isDirectory(oldPath) && isWindowsOS() && !isWSLPath(oldPath) && hasSubDirectories(oldPath);
|
||||
// let parentDirUnwatched = false;
|
||||
// let parentDirRewatched = false;
|
||||
|
||||
try {
|
||||
// Normalize paths if they are WSL paths
|
||||
oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath);
|
||||
@ -358,33 +375,80 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
const newBruFilePath = bruFile.replace(oldPath, newPath);
|
||||
moveRequestUid(bruFile, newBruFilePath);
|
||||
}
|
||||
return fs.renameSync(oldPath, newPath);
|
||||
|
||||
// watcher.unlinkItemPathInWatcher(parentDir);
|
||||
// parentDirUnwatched = true;
|
||||
|
||||
/**
|
||||
* If it is windows OS
|
||||
* And it is not WSL path (meaning its not linux running on windows using WSL)
|
||||
* And it has sub directories
|
||||
* Only then we need to use the temp dir approach to rename the folder
|
||||
*
|
||||
* Windows OS would sometimes throw error when renaming a folder with sub directories
|
||||
* This is a alternative approach to avoid that error
|
||||
*/
|
||||
if (isWindowsOSAndNotWSLAndItemHasSubDirectories) {
|
||||
await fsExtra.copy(oldPath, tempDir);
|
||||
await fsExtra.remove(oldPath);
|
||||
await fsExtra.move(tempDir, newPath, { overwrite: true });
|
||||
await fsExtra.remove(tempDir);
|
||||
} else {
|
||||
await fs.renameSync(oldPath, newPath);
|
||||
}
|
||||
// watcher.addItemPathInWatcher(parentDir);
|
||||
// parentDirRewatched = true;
|
||||
|
||||
return newPath;
|
||||
}
|
||||
|
||||
const isBru = hasBruExtension(oldPath);
|
||||
if (!isBru) {
|
||||
if (!hasBruExtension(oldPath)) {
|
||||
throw new Error(`path: ${oldPath} is not a bru file`);
|
||||
}
|
||||
|
||||
// update name in file and save new copy, then delete old copy
|
||||
const data = fs.readFileSync(oldPath, 'utf8');
|
||||
const jsonData = bruToJson(data);
|
||||
if (!isValidFilename(newName)) {
|
||||
throw new Error(`path: ${newName} is not a valid filename`);
|
||||
}
|
||||
|
||||
// update name in file and save new copy, then delete old copy
|
||||
const data = await fs.promises.readFile(oldPath, 'utf8'); // Use async read
|
||||
const jsonData = bruToJson(data);
|
||||
jsonData.name = newName;
|
||||
moveRequestUid(oldPath, newPath);
|
||||
|
||||
const content = jsonToBru(jsonData);
|
||||
await fs.unlinkSync(oldPath);
|
||||
await fs.promises.unlink(oldPath);
|
||||
await writeFile(newPath, content);
|
||||
|
||||
return newPath;
|
||||
} catch (error) {
|
||||
// in case an error occurs during the rename file operations after unlinking the parent dir
|
||||
// and the rewatch fails, we need to add it back to watcher
|
||||
// if (parentDirUnwatched && !parentDirRewatched) {
|
||||
// watcher.addItemPathInWatcher(parentDir);
|
||||
// }
|
||||
|
||||
// in case the rename file operations fails, and we see that the temp dir exists
|
||||
// and the old path does not exist, we need to restore the data from the temp dir to the old path
|
||||
if (isWindowsOSAndNotWSLAndItemHasSubDirectories) {
|
||||
if (fsExtra.pathExistsSync(tempDir) && !fsExtra.pathExistsSync(oldPath)) {
|
||||
try {
|
||||
await fsExtra.copy(tempDir, oldPath);
|
||||
await fsExtra.remove(tempDir);
|
||||
} catch (err) {
|
||||
console.error("Failed to restore data to the old path:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// new folder
|
||||
ipcMain.handle('renderer:new-folder', async (event, pathname) => {
|
||||
const resolvedFolderName = sanitizeDirectoryName(path.basename(pathname));
|
||||
pathname = path.join(path.dirname(pathname), resolvedFolderName);
|
||||
try {
|
||||
if (!fs.existsSync(pathname)) {
|
||||
fs.mkdirSync(pathname);
|
||||
@ -443,7 +507,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => {
|
||||
try {
|
||||
let collectionName = sanitizeDirectoryName(collection.name);
|
||||
let collectionName = sanitizeCollectionName(collection.name);
|
||||
let collectionPath = path.join(collectionLocation, collectionName);
|
||||
|
||||
if (fs.existsSync(collectionPath)) {
|
||||
@ -459,6 +523,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
fs.writeFileSync(filePath, content);
|
||||
}
|
||||
if (item.type === 'folder') {
|
||||
item.name = sanitizeDirectoryName(item.name);
|
||||
const folderPath = path.join(currentPath, item.name);
|
||||
fs.mkdirSync(folderPath);
|
||||
|
||||
|
@ -2,7 +2,7 @@ const crypto = require('crypto');
|
||||
const { URL } = require('url');
|
||||
|
||||
function isStrPresent(str) {
|
||||
return str && str !== '' && str !== 'undefined';
|
||||
return str && str.trim() !== '' && str.trim() !== 'undefined';
|
||||
}
|
||||
|
||||
function stripQuotes(str) {
|
||||
@ -15,7 +15,10 @@ function containsDigestHeader(response) {
|
||||
}
|
||||
|
||||
function containsAuthorizationHeader(originalRequest) {
|
||||
return Boolean(originalRequest.headers['Authorization']);
|
||||
return Boolean(
|
||||
originalRequest.headers['Authorization'] ||
|
||||
originalRequest.headers['authorization']
|
||||
);
|
||||
}
|
||||
|
||||
function md5(input) {
|
||||
@ -24,11 +27,10 @@ function md5(input) {
|
||||
|
||||
function addDigestInterceptor(axiosInstance, request) {
|
||||
const { username, password } = request.digestConfig;
|
||||
|
||||
console.debug(request);
|
||||
console.debug('Digest Auth Interceptor Initialized');
|
||||
|
||||
if (!isStrPresent(username) || !isStrPresent(password)) {
|
||||
console.warn('Required Digest Auth fields are not present');
|
||||
console.warn('Required Digest Auth fields (username/password) are not present');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -37,41 +39,82 @@ function addDigestInterceptor(axiosInstance, request) {
|
||||
(error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
// Prevent retry loops
|
||||
if (originalRequest._retry) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
originalRequest._retry = true;
|
||||
|
||||
if (
|
||||
error.response?.status === 401 &&
|
||||
containsDigestHeader(error.response) &&
|
||||
!containsAuthorizationHeader(originalRequest)
|
||||
) {
|
||||
console.debug('Processing Digest Authentication Challenge');
|
||||
console.debug(error.response.headers['www-authenticate']);
|
||||
|
||||
const authDetails = error.response.headers['www-authenticate']
|
||||
.split(', ')
|
||||
.map((v) => v.split('=').map(stripQuotes))
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
|
||||
console.debug(authDetails);
|
||||
.split(',')
|
||||
.map((pair) => pair.split('=').map((item) => item.trim()).map(stripQuotes))
|
||||
.reduce((acc, [key, value]) => {
|
||||
const normalizedKey = key.toLowerCase().replace('digest ', '');
|
||||
if (normalizedKey && value !== undefined) {
|
||||
acc[normalizedKey] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Validate required auth details
|
||||
if (!authDetails.realm || !authDetails.nonce) {
|
||||
console.warn('Missing required auth details (realm or nonce)');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
console.debug("Auth Details: \n", authDetails);
|
||||
|
||||
const nonceCount = '00000001';
|
||||
const cnonce = crypto.randomBytes(24).toString('hex');
|
||||
|
||||
if (authDetails.algorithm && authDetails.algorithm.toUpperCase() !== 'MD5') {
|
||||
console.warn(`Unsupported Digest algorithm: ${algo}`);
|
||||
console.warn(`Unsupported Digest algorithm: ${authDetails.algorithm}`);
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
authDetails.algorithm = 'MD5';
|
||||
}
|
||||
const uri = new URL(request.url).pathname;
|
||||
const HA1 = md5(`${username}:${authDetails['Digest realm']}:${password}`);
|
||||
const HA2 = md5(`${request.method}:${uri}`);
|
||||
const response = md5(`${HA1}:${authDetails.nonce}:${nonceCount}:${cnonce}:auth:${HA2}`);
|
||||
|
||||
const authorizationHeader =
|
||||
`Digest username="${username}",realm="${authDetails['Digest realm']}",` +
|
||||
`nonce="${authDetails.nonce}",uri="${uri}",qop="auth",algorithm="${authDetails.algorithm}",` +
|
||||
`response="${response}",nc="${nonceCount}",cnonce="${cnonce}"`;
|
||||
const uri = new URL(request.url, request.baseURL || 'http://localhost').pathname; // Handle relative URLs
|
||||
const HA1 = md5(`${username}:${authDetails.realm}:${password}`);
|
||||
const HA2 = md5(`${request.method}:${uri}`);
|
||||
const response = md5(
|
||||
`${HA1}:${authDetails.nonce}:${nonceCount}:${cnonce}:auth:${HA2}`
|
||||
);
|
||||
|
||||
const headerFields = [
|
||||
`username="${username}"`,
|
||||
`realm="${authDetails.realm}"`,
|
||||
`nonce="${authDetails.nonce}"`,
|
||||
`uri="${uri}"`,
|
||||
`qop="auth"`,
|
||||
`algorithm="${authDetails.algorithm}"`,
|
||||
`response="${response}"`,
|
||||
`nc="${nonceCount}"`,
|
||||
`cnonce="${cnonce}"`,
|
||||
];
|
||||
|
||||
if (authDetails.opaque) {
|
||||
headerFields.push(`opaque="${authDetails.opaque}"`);
|
||||
}
|
||||
|
||||
const authorizationHeader = `Digest ${headerFields.join(', ')}`;
|
||||
|
||||
// Ensure headers are initialized
|
||||
originalRequest.headers = originalRequest.headers || {};
|
||||
originalRequest.headers['Authorization'] = authorizationHeader;
|
||||
|
||||
console.debug(`Authorization: ${originalRequest.headers['Authorization']}`);
|
||||
|
||||
delete originalRequest.digestConfig;
|
||||
|
||||
return axiosInstance(originalRequest);
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,8 @@ const {
|
||||
const Oauth2Store = require('../../store/oauth2');
|
||||
const iconv = require('iconv-lite');
|
||||
const FormData = require('form-data');
|
||||
const { createFormData } = prepareRequest;
|
||||
const { createFormData } = require('../../utils/form-data');
|
||||
const { findItemInCollectionByPathname } = require('../../utils/collection');
|
||||
|
||||
const safeStringifyJSON = (data) => {
|
||||
try {
|
||||
@ -373,10 +374,14 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
|
||||
// Filter out ZWNBSP character
|
||||
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
|
||||
data = data.replace(/^\uFEFF/, '');
|
||||
if (!disableParsingResponseJson) {
|
||||
|
||||
// If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed
|
||||
if ( !disableParsingResponseJson && ! (typeof data === 'string' && data.startsWith("\"") && data.endsWith("\""))) {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
} catch { }
|
||||
} catch {
|
||||
console.log('Failed to parse response data as JSON');
|
||||
}
|
||||
|
||||
return { data, dataBuffer };
|
||||
};
|
||||
@ -400,11 +405,12 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
) => {
|
||||
// run pre-request script
|
||||
let scriptResult;
|
||||
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(os.EOL);
|
||||
const requestScript = get(request, 'script.req');
|
||||
if (requestScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
scriptResult = await scriptRuntime.runRequestScript(
|
||||
@ -415,7 +421,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
@ -465,7 +472,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
) => {
|
||||
// run post-response vars
|
||||
const postResponseVars = get(request, 'vars.res', []);
|
||||
@ -500,12 +508,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
// run post-response script
|
||||
const responseScript = compact(scriptingConfig.flow === 'sequential' ? [
|
||||
get(collectionRoot, 'request.script.res'), get(request, 'script.res')
|
||||
] : [
|
||||
get(request, 'script.res'), get(collectionRoot, 'request.script.res')
|
||||
]).join(os.EOL);
|
||||
|
||||
const responseScript = get(request, 'script.res');
|
||||
let scriptResult;
|
||||
if (responseScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
@ -518,7 +521,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
@ -535,14 +539,28 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
return scriptResult;
|
||||
};
|
||||
|
||||
// handler for sending http request
|
||||
ipcMain.handle('send-http-request', async (event, item, collection, environment, runtimeVariables) => {
|
||||
const runRequest = async ({ item, collection, environment, runtimeVariables, runInBackground = false }) => {
|
||||
const collectionUid = collection.uid;
|
||||
const collectionPath = collection.pathname;
|
||||
const cancelTokenUid = uuid();
|
||||
const requestUid = uuid();
|
||||
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
const runRequestByItemPathname = async (relativeItemPathname) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let itemPathname = path.join(collection?.pathname, relativeItemPathname);
|
||||
if (itemPathname && !itemPathname?.endsWith('.bru')) {
|
||||
itemPathname = `${itemPathname}.bru`;
|
||||
}
|
||||
const _item = findItemInCollectionByPathname(collection, itemPathname);
|
||||
if(_item) {
|
||||
const res = await runRequest({ item: _item, collection, environment, runtimeVariables, runInBackground: true });
|
||||
resolve(res);
|
||||
}
|
||||
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
|
||||
});
|
||||
}
|
||||
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'request-queued',
|
||||
requestUid,
|
||||
collectionUid,
|
||||
@ -573,7 +591,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
|
||||
const axiosInstance = await configureRequest(
|
||||
@ -586,7 +605,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
onConsoleLog
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'request-sent',
|
||||
requestSent: {
|
||||
url: request.url,
|
||||
@ -658,7 +677,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
|
||||
// run assertions
|
||||
@ -674,7 +694,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'assertion-results',
|
||||
results: results,
|
||||
itemUid: item.uid,
|
||||
@ -683,14 +703,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
}
|
||||
|
||||
// run tests
|
||||
const testScript = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
||||
const testFile = compact(scriptingConfig.flow === 'sequential' ? [
|
||||
get(collectionRoot, 'request.tests'), testScript,
|
||||
] : [
|
||||
testScript, get(collectionRoot, 'request.tests')
|
||||
]).join(os.EOL);
|
||||
|
||||
const testFile = get(request, 'tests');
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const testResults = await testRuntime.runTests(
|
||||
@ -702,10 +715,11 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'test-results',
|
||||
results: testResults.results,
|
||||
itemUid: item.uid,
|
||||
@ -739,6 +753,11 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// handler for sending http request
|
||||
ipcMain.handle('send-http-request', async (event, item, collection, environment, runtimeVariables) => {
|
||||
return await runRequest({ item, collection, environment, runtimeVariables });
|
||||
});
|
||||
|
||||
ipcMain.handle('send-collection-oauth2-request', async (event, collection, environment, runtimeVariables) => {
|
||||
@ -932,10 +951,26 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
let stopRunnerExecution = false;
|
||||
|
||||
const abortController = new AbortController();
|
||||
saveCancelToken(cancelTokenUid, abortController);
|
||||
|
||||
const runRequestByItemPathname = async (relativeItemPathname) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let itemPathname = path.join(collection?.pathname, relativeItemPathname);
|
||||
if (itemPathname && !itemPathname?.endsWith('.bru')) {
|
||||
itemPathname = `${itemPathname}.bru`;
|
||||
}
|
||||
const _item = findItemInCollectionByPathname(collection, itemPathname);
|
||||
if(_item) {
|
||||
const res = await runRequest({ item: _item, collection, environment, runtimeVariables, runInBackground: true });
|
||||
resolve(res);
|
||||
}
|
||||
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (!folder) {
|
||||
folder = collection;
|
||||
}
|
||||
@ -978,7 +1013,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const item = folderRequests[currentRequestIndex];
|
||||
stopRunnerExecution = false;
|
||||
|
||||
const item = cloneDeep(folderRequests[currentRequestIndex]);
|
||||
let nextRequestName;
|
||||
const itemUid = item.uid;
|
||||
const eventData = {
|
||||
@ -1011,13 +1048,33 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
|
||||
if (preRequestScriptResult?.nextRequestName !== undefined) {
|
||||
nextRequestName = preRequestScriptResult.nextRequestName;
|
||||
}
|
||||
|
||||
if (preRequestScriptResult?.stopExecution) {
|
||||
stopRunnerExecution = true;
|
||||
}
|
||||
|
||||
if (preRequestScriptResult?.skipRequest) {
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'runner-request-skipped',
|
||||
error: 'Request has been skipped from pre-request script',
|
||||
responseReceived: {
|
||||
status: 'skipped',
|
||||
statusText: 'request skipped via pre-request script',
|
||||
data: null
|
||||
},
|
||||
...eventData
|
||||
});
|
||||
currentRequestIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// todo:
|
||||
// i have no clue why electron can't send the request object
|
||||
// without safeParseJSON(safeStringifyJSON(request.data))
|
||||
@ -1130,13 +1187,18 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
|
||||
if (postRequestScriptResult?.nextRequestName !== undefined) {
|
||||
nextRequestName = postRequestScriptResult.nextRequestName;
|
||||
}
|
||||
|
||||
if (postRequestScriptResult?.stopExecution) {
|
||||
stopRunnerExecution = true;
|
||||
}
|
||||
|
||||
// run assertions
|
||||
const assertions = get(item, 'request.assertions');
|
||||
if (assertions) {
|
||||
@ -1158,14 +1220,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
}
|
||||
|
||||
// run tests
|
||||
const testScript = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
||||
const testFile = compact(scriptingConfig.flow === 'sequential' ? [
|
||||
get(collectionRoot, 'request.tests'), testScript
|
||||
] : [
|
||||
testScript, get(collectionRoot, 'request.tests')
|
||||
]).join(os.EOL);
|
||||
|
||||
const testFile = get(request, 'tests');
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const testResults = await testRuntime.runTests(
|
||||
@ -1177,7 +1232,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
|
||||
if (testResults?.nextRequestName !== undefined) {
|
||||
@ -1208,6 +1264,18 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
...eventData
|
||||
});
|
||||
}
|
||||
|
||||
if (stopRunnerExecution) {
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'testrun-ended',
|
||||
collectionUid,
|
||||
folderUid,
|
||||
statusText: 'collection run was terminated!'
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (nextRequestName !== undefined) {
|
||||
nJumps++;
|
||||
if (nJumps > 10000) {
|
||||
|
@ -86,11 +86,12 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
} catch (err) {}
|
||||
}
|
||||
} else if (contentType === 'multipart/form-data') {
|
||||
if (typeof request.data === 'object' && !(request.data instanceof FormData)) {
|
||||
if (Array.isArray(request?.data) && !(request.data instanceof FormData)) {
|
||||
try {
|
||||
forOwn(request?.data, (value, key) => {
|
||||
request.data[key] = _interpolate(value);
|
||||
});
|
||||
request.data = request?.data?.map(d => ({
|
||||
...d,
|
||||
value: _interpolate(d?.value)
|
||||
}));
|
||||
} catch (err) {}
|
||||
}
|
||||
} else {
|
||||
|
@ -1,194 +1,8 @@
|
||||
const os = require('os');
|
||||
const { get, each, filter, compact, forOwn } = require('lodash');
|
||||
const { get, each, filter } = require('lodash');
|
||||
const decomment = require('decomment');
|
||||
const FormData = require('form-data');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('node:crypto');
|
||||
const { getTreePathFromCollectionToItem } = require('../../utils/collection');
|
||||
const { buildFormUrlEncodedPayload } = require('../../utils/common');
|
||||
|
||||
const mergeFolderLevelHeaders = (request, requestTreePath) => {
|
||||
let folderHeaders = new Map();
|
||||
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let headers = get(i, 'root.request.headers', []);
|
||||
headers.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
folderHeaders.set(header.name, header.value);
|
||||
}
|
||||
});
|
||||
} else if (i.uid === request.uid) {
|
||||
const headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
|
||||
headers.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
folderHeaders.set(header.name, header.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mergedFolderHeaders = Array.from(folderHeaders, ([name, value]) => ({ name, value, enabled: true }));
|
||||
let requestHeaders = request.headers || [];
|
||||
let requestHeadersMap = new Map();
|
||||
|
||||
for (let header of requestHeaders) {
|
||||
if (header.enabled) {
|
||||
requestHeadersMap.set(header.name, header.value);
|
||||
}
|
||||
}
|
||||
|
||||
mergedFolderHeaders.forEach((header) => {
|
||||
requestHeadersMap.set(header.name, header.value);
|
||||
});
|
||||
|
||||
request.headers = Array.from(requestHeadersMap, ([name, value]) => ({ name, value, enabled: true }));
|
||||
};
|
||||
|
||||
const mergeVars = (collection, request, requestTreePath) => {
|
||||
let reqVars = new Map();
|
||||
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
|
||||
let collectionVariables = {};
|
||||
collectionRequestVars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
reqVars.set(_var.name, _var.value);
|
||||
collectionVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
let folderVariables = {};
|
||||
let requestVariables = {};
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
reqVars.set(_var.name, _var.value);
|
||||
folderVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
reqVars.set(_var.name, _var.value);
|
||||
requestVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
request.collectionVariables = collectionVariables;
|
||||
request.folderVariables = folderVariables;
|
||||
request.requestVariables = requestVariables;
|
||||
|
||||
request.vars.req = Array.from(reqVars, ([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
enabled: true,
|
||||
type: 'request'
|
||||
}));
|
||||
|
||||
let resVars = new Map();
|
||||
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
|
||||
collectionResponseVars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
resVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.res', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
resVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
resVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
request.vars.res = Array.from(resVars, ([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
enabled: true,
|
||||
type: 'response'
|
||||
}));
|
||||
};
|
||||
|
||||
const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => {
|
||||
let folderCombinedPreReqScript = [];
|
||||
let folderCombinedPostResScript = [];
|
||||
let folderCombinedTests = [];
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let preReqScript = get(i, 'root.request.script.req', '');
|
||||
if (preReqScript && preReqScript.trim() !== '') {
|
||||
folderCombinedPreReqScript.push(preReqScript);
|
||||
}
|
||||
|
||||
let postResScript = get(i, 'root.request.script.res', '');
|
||||
if (postResScript && postResScript.trim() !== '') {
|
||||
folderCombinedPostResScript.push(postResScript);
|
||||
}
|
||||
|
||||
let tests = get(i, 'root.request.tests', '');
|
||||
if (tests && tests?.trim?.() !== '') {
|
||||
folderCombinedTests.push(tests);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (folderCombinedPreReqScript.length) {
|
||||
request.script.req = compact([...folderCombinedPreReqScript, request?.script?.req || '']).join(os.EOL);
|
||||
}
|
||||
|
||||
if (folderCombinedPostResScript.length) {
|
||||
if (scriptFlow === 'sequential') {
|
||||
request.script.res = compact([...folderCombinedPostResScript, request?.script?.res || '']).join(os.EOL);
|
||||
} else {
|
||||
request.script.res = compact([request?.script?.res || '', ...folderCombinedPostResScript.reverse()]).join(os.EOL);
|
||||
}
|
||||
}
|
||||
|
||||
if (folderCombinedTests.length) {
|
||||
if (scriptFlow === 'sequential') {
|
||||
request.tests = compact([...folderCombinedTests, request?.tests || '']).join(os.EOL);
|
||||
} else {
|
||||
request.tests = compact([request?.tests || '', ...folderCombinedTests.reverse()]).join(os.EOL);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createFormData = (datas, collectionPath) => {
|
||||
// make axios work in node using form data
|
||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||
const form = new FormData();
|
||||
forOwn(datas, (value, key) => {
|
||||
if (typeof value == 'string') {
|
||||
form.append(key, value);
|
||||
return;
|
||||
}
|
||||
|
||||
const filePaths = value || [];
|
||||
filePaths?.forEach?.((filePath) => {
|
||||
let trimmedFilePath = filePath.trim();
|
||||
|
||||
if (!path.isAbsolute(trimmedFilePath)) {
|
||||
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
|
||||
}
|
||||
|
||||
form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
|
||||
});
|
||||
});
|
||||
return form;
|
||||
};
|
||||
const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars } = require('../../utils/collection');
|
||||
const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data');
|
||||
|
||||
const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
@ -352,29 +166,25 @@ const prepareRequest = (item, collection) => {
|
||||
const headers = {};
|
||||
let contentTypeDefined = false;
|
||||
let url = request.url;
|
||||
|
||||
// Collection level headers
|
||||
|
||||
each(get(collectionRoot, 'request.headers', []), (h) => {
|
||||
if (h.enabled && h.name.length > 0) {
|
||||
headers[h.name] = h.value;
|
||||
if (h.name.toLowerCase() === 'content-type') {
|
||||
contentTypeDefined = true;
|
||||
}
|
||||
if (h.enabled && h.name?.toLowerCase() === 'content-type') {
|
||||
contentTypeDefined = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// scriptFlow is either "sandwich" or "sequential"
|
||||
|
||||
const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich';
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
if (requestTreePath && requestTreePath.length > 0) {
|
||||
mergeFolderLevelHeaders(request, requestTreePath);
|
||||
mergeFolderLevelScripts(request, requestTreePath, scriptFlow);
|
||||
mergeHeaders(collection, request, requestTreePath);
|
||||
mergeScripts(collection, request, requestTreePath, scriptFlow);
|
||||
mergeVars(collection, request, requestTreePath);
|
||||
request.globalEnvironmentVariables = collection?.globalEnvironmentVariables;
|
||||
}
|
||||
|
||||
// Request level headers
|
||||
each(request.headers, (h) => {
|
||||
|
||||
each(get(request, 'headers', []), (h) => {
|
||||
if (h.enabled && h.name.length > 0) {
|
||||
headers[h.name] = h.value;
|
||||
if (h.name.toLowerCase() === 'content-type') {
|
||||
@ -414,7 +224,7 @@ const prepareRequest = (item, collection) => {
|
||||
|
||||
if (request.body.mode === 'xml') {
|
||||
if (!contentTypeDefined) {
|
||||
axiosRequest.headers['content-type'] = 'text/xml';
|
||||
axiosRequest.headers['content-type'] = 'application/xml';
|
||||
}
|
||||
axiosRequest.data = request.body.xml;
|
||||
}
|
||||
@ -435,11 +245,11 @@ const prepareRequest = (item, collection) => {
|
||||
}
|
||||
|
||||
if (request.body.mode === 'multipartForm') {
|
||||
axiosRequest.headers['content-type'] = 'multipart/form-data';
|
||||
const params = {};
|
||||
if (!contentTypeDefined) {
|
||||
axiosRequest.headers['content-type'] = 'multipart/form-data';
|
||||
}
|
||||
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
|
||||
each(enabledParams, (p) => (params[p.name] = p.value));
|
||||
axiosRequest.data = params;
|
||||
axiosRequest.data = enabledParams;
|
||||
}
|
||||
|
||||
if (request.body.mode === 'graphql') {
|
||||
@ -458,6 +268,10 @@ const prepareRequest = (item, collection) => {
|
||||
axiosRequest.script = request.script;
|
||||
}
|
||||
|
||||
if (request.tests) {
|
||||
axiosRequest.tests = request.tests;
|
||||
}
|
||||
|
||||
axiosRequest.vars = request.vars;
|
||||
axiosRequest.collectionVariables = request.collectionVariables;
|
||||
axiosRequest.folderVariables = request.folderVariables;
|
||||
@ -470,4 +284,3 @@ const prepareRequest = (item, collection) => {
|
||||
|
||||
module.exports = prepareRequest;
|
||||
module.exports.setAuthHeaders = setAuthHeaders;
|
||||
module.exports.createFormData = createFormData;
|
||||
|
@ -1,4 +1,4 @@
|
||||
const { ipcRenderer, contextBridge } = require('electron');
|
||||
const { ipcRenderer, contextBridge, webUtils } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('ipcRenderer', {
|
||||
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
||||
@ -10,5 +10,9 @@ contextBridge.exposeInMainWorld('ipcRenderer', {
|
||||
return () => {
|
||||
ipcRenderer.removeListener(channel, subscription);
|
||||
};
|
||||
},
|
||||
getFilePath (file) {
|
||||
const path = webUtils.getPathForFile(file)
|
||||
return path;
|
||||
}
|
||||
});
|
||||
|
@ -1,5 +1,161 @@
|
||||
const each = require('lodash/each');
|
||||
const find = require('lodash/find');
|
||||
const { get, each, find, compact } = require('lodash');
|
||||
const os = require('os');
|
||||
|
||||
const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let headers = new Map();
|
||||
|
||||
let collectionHeaders = get(collection, 'root.request.headers', []);
|
||||
collectionHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header.value);
|
||||
if (header?.name?.toLowerCase() === 'content-type') {
|
||||
contentTypeDefined = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let _headers = get(i, 'root.request.headers', []);
|
||||
_headers.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
|
||||
_headers.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
request.headers = Array.from(headers, ([name, value]) => ({ name, value, enabled: true }));
|
||||
};
|
||||
|
||||
const mergeVars = (collection, request, requestTreePath) => {
|
||||
let reqVars = new Map();
|
||||
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
|
||||
let collectionVariables = {};
|
||||
collectionRequestVars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
reqVars.set(_var.name, _var.value);
|
||||
collectionVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
let folderVariables = {};
|
||||
let requestVariables = {};
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
reqVars.set(_var.name, _var.value);
|
||||
folderVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
reqVars.set(_var.name, _var.value);
|
||||
requestVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
request.collectionVariables = collectionVariables;
|
||||
request.folderVariables = folderVariables;
|
||||
request.requestVariables = requestVariables;
|
||||
|
||||
if(request?.vars) {
|
||||
request.vars.req = Array.from(reqVars, ([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
enabled: true,
|
||||
type: 'request'
|
||||
}));
|
||||
}
|
||||
|
||||
let resVars = new Map();
|
||||
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
|
||||
collectionResponseVars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
resVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.res', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
resVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
resVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(request?.vars) {
|
||||
request.vars.res = Array.from(resVars, ([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
enabled: true,
|
||||
type: 'response'
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
let collectionPreReqScript = get(collection, 'root.request.script.req', '');
|
||||
let collectionPostResScript = get(collection, 'root.request.script.res', '');
|
||||
let collectionTests = get(collection, 'root.request.tests', '');
|
||||
|
||||
let combinedPreReqScript = [];
|
||||
let combinedPostResScript = [];
|
||||
let combinedTests = [];
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let preReqScript = get(i, 'root.request.script.req', '');
|
||||
if (preReqScript && preReqScript.trim() !== '') {
|
||||
combinedPreReqScript.push(preReqScript);
|
||||
}
|
||||
|
||||
let postResScript = get(i, 'root.request.script.res', '');
|
||||
if (postResScript && postResScript.trim() !== '') {
|
||||
combinedPostResScript.push(postResScript);
|
||||
}
|
||||
|
||||
let tests = get(i, 'root.request.tests', '');
|
||||
if (tests && tests?.trim?.() !== '') {
|
||||
combinedTests.push(tests);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request.script.req = compact([collectionPreReqScript, ...combinedPreReqScript, request?.script?.req || '']).join(os.EOL);
|
||||
|
||||
if (scriptFlow === 'sequential') {
|
||||
request.script.res = compact([collectionPostResScript, ...combinedPostResScript, request?.script?.res || '']).join(os.EOL);
|
||||
} else {
|
||||
request.script.res = compact([request?.script?.res || '', ...combinedPostResScript.reverse(), collectionPostResScript]).join(os.EOL);
|
||||
}
|
||||
|
||||
if (scriptFlow === 'sequential') {
|
||||
request.tests = compact([collectionTests, ...combinedTests, request?.tests || '']).join(os.EOL);
|
||||
} else {
|
||||
request.tests = compact([request?.tests || '', ...combinedTests.reverse(), collectionTests]).join(os.EOL);
|
||||
}
|
||||
};
|
||||
|
||||
const flattenItems = (items = []) => {
|
||||
const flattenedItems = [];
|
||||
@ -44,14 +200,34 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item.uid);
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
flattenItems,
|
||||
findItem,
|
||||
findItemInCollection,
|
||||
findParentItemInCollection,
|
||||
getTreePathFromCollectionToItem
|
||||
const slash = (path) => {
|
||||
const isExtendedLengthPath = /^\\\\\?\\/.test(path);
|
||||
if (isExtendedLengthPath) {
|
||||
return path;
|
||||
}
|
||||
return path?.replace?.(/\\/g, '/');
|
||||
};
|
||||
|
||||
const findItemByPathname = (items = [], pathname) => {
|
||||
return find(items, (i) => slash(i.pathname) === slash(pathname));
|
||||
};
|
||||
|
||||
const findItemInCollectionByPathname = (collection, pathname) => {
|
||||
let flattenedItems = flattenItems(collection.items);
|
||||
|
||||
return findItemByPathname(flattenedItems, pathname);
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
mergeHeaders,
|
||||
mergeVars,
|
||||
mergeScripts,
|
||||
getTreePathFromCollectionToItem,
|
||||
slash,
|
||||
findItemByPathname,
|
||||
findItemInCollectionByPathname
|
||||
}
|
@ -85,24 +85,6 @@ const flattenDataForDotNotation = (data) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Array.<object>} params The request body Array
|
||||
* @returns {object} Returns an obj with repeating key as a array of values
|
||||
* {item: 2, item: 3, item1: 4} becomes {item: [2,3], item1: 4}
|
||||
*/
|
||||
const buildFormUrlEncodedPayload = (params) => {
|
||||
return params.reduce((acc, p) => {
|
||||
if (!acc[p.name]) {
|
||||
acc[p.name] = p.value;
|
||||
} else if (Array.isArray(acc[p.name])) {
|
||||
acc[p.name].push(p.value);
|
||||
} else {
|
||||
acc[p.name] = [acc[p.name], p.value];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
uuid,
|
||||
stringifyJson,
|
||||
@ -111,6 +93,5 @@ module.exports = {
|
||||
safeParseJSON,
|
||||
simpleHash,
|
||||
generateUidBasedOnHash,
|
||||
flattenDataForDotNotation,
|
||||
buildFormUrlEncodedPayload
|
||||
flattenDataForDotNotation
|
||||
};
|
||||
|
@ -38,6 +38,11 @@ const isDirectory = (dirPath) => {
|
||||
}
|
||||
};
|
||||
|
||||
const hasSubDirectories = (dir) => {
|
||||
const files = fs.readdirSync(dir);
|
||||
return files.some(file => fs.statSync(path.join(dir, file)).isDirectory());
|
||||
};
|
||||
|
||||
const normalizeAndResolvePath = (pathname) => {
|
||||
if (isSymbolicLink(pathname)) {
|
||||
const absPath = path.dirname(pathname);
|
||||
@ -156,8 +161,30 @@ const searchForBruFiles = (dir) => {
|
||||
return searchForFiles(dir, '.bru');
|
||||
};
|
||||
|
||||
const sanitizeCollectionName = (name) => {
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
const sanitizeDirectoryName = (name) => {
|
||||
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-');
|
||||
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-').trim();
|
||||
};
|
||||
|
||||
const isWindowsOS = () => {
|
||||
return os.platform() === 'win32';
|
||||
}
|
||||
|
||||
const isValidFilename = (fileName) => {
|
||||
const inValidChars = /[\\/:*?"<>|]/;
|
||||
|
||||
if (!fileName || inValidChars.test(fileName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fileName.endsWith(' ') || fileName.endsWith('.') || fileName.startsWith('.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const safeToRename = (oldPath, newPath) => {
|
||||
@ -170,7 +197,7 @@ const safeToRename = (oldPath, newPath) => {
|
||||
const oldStat = fs.statSync(oldPath);
|
||||
const newStat = fs.statSync(newPath);
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
if (isWindowsOS()) {
|
||||
// Windows-specific comparison:
|
||||
// Check if both files have the same birth time, size (Since, Win FAT-32 doesn't use inodes)
|
||||
|
||||
@ -204,5 +231,9 @@ module.exports = {
|
||||
searchForFiles,
|
||||
searchForBruFiles,
|
||||
sanitizeDirectoryName,
|
||||
safeToRename
|
||||
sanitizeCollectionName,
|
||||
isWindowsOS,
|
||||
safeToRename,
|
||||
isValidFilename,
|
||||
hasSubDirectories
|
||||
};
|
||||
|
62
packages/bruno-electron/src/utils/form-data.js
Normal file
62
packages/bruno-electron/src/utils/form-data.js
Normal file
@ -0,0 +1,62 @@
|
||||
const { forEach } = require('lodash');
|
||||
const FormData = require('form-data');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* @param {Array.<object>} params The request body Array
|
||||
* @returns {object} Returns an obj with repeating key as a array of values
|
||||
* {item: 2, item: 3, item1: 4} becomes {item: [2,3], item1: 4}
|
||||
*/
|
||||
const buildFormUrlEncodedPayload = (params) => {
|
||||
return params.reduce((acc, p) => {
|
||||
if (!acc[p.name]) {
|
||||
acc[p.name] = p.value;
|
||||
} else if (Array.isArray(acc[p.name])) {
|
||||
acc[p.name].push(p.value);
|
||||
} else {
|
||||
acc[p.name] = [acc[p.name], p.value];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
|
||||
const createFormData = (data, collectionPath) => {
|
||||
// make axios work in node using form data
|
||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||
const form = new FormData();
|
||||
forEach(data, (datum) => {
|
||||
const { name, type, value, contentType } = datum;
|
||||
let options = {};
|
||||
if (contentType) {
|
||||
options.contentType = contentType;
|
||||
}
|
||||
if (type === 'text') {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => form.append(name, val, options));
|
||||
} else {
|
||||
form.append(name, value, options);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'file') {
|
||||
const filePaths = value || [];
|
||||
filePaths.forEach((filePath) => {
|
||||
let trimmedFilePath = filePath.trim();
|
||||
if (!path.isAbsolute(trimmedFilePath)) {
|
||||
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
|
||||
}
|
||||
options.filename = path.basename(trimmedFilePath);
|
||||
form.append(name, fs.createReadStream(trimmedFilePath), options);
|
||||
});
|
||||
}
|
||||
});
|
||||
return form;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
buildFormUrlEncodedPayload,
|
||||
createFormData
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
|
||||
const prepareRequest = require('../../src/ipc/network/prepare-request');
|
||||
const { buildFormUrlEncodedPayload } = require('../../src/utils/common');
|
||||
const { buildFormUrlEncodedPayload } = require('../../src/utils/form-data');
|
||||
|
||||
describe('prepare-request: prepareRequest', () => {
|
||||
describe('Decomments request body', () => {
|
||||
|
@ -20,7 +20,7 @@
|
||||
"graphql": "^16.6.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"postcss": "8.4.47",
|
||||
"react": "18.2.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "18.2.0",
|
||||
"rollup":"3.29.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
|
@ -16,6 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/crypto-js": "^3.1.9",
|
||||
"@usebruno/query": "0.1.0",
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
@ -25,11 +26,10 @@
|
||||
"chai": "^4.3.7",
|
||||
"chai-string": "^1.5.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"crypto-js-3.1.9-1": "npm:crypto-js@^3.1.9-1",
|
||||
"json-query": "^2.2.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "3.3.4",
|
||||
"nanoid": "3.3.8",
|
||||
"node-fetch": "^2.7.0",
|
||||
"node-vault": "^0.10.2",
|
||||
"path": "^0.12.7",
|
||||
|
@ -13,6 +13,17 @@ class Bru {
|
||||
this.requestVariables = requestVariables || {};
|
||||
this.globalEnvironmentVariables = globalEnvironmentVariables || {};
|
||||
this.collectionPath = collectionPath;
|
||||
this.runner = {
|
||||
skipRequest: () => {
|
||||
this.skipRequest = true;
|
||||
},
|
||||
stopExecution: () => {
|
||||
this.stopExecution = true;
|
||||
},
|
||||
setNextRequest: (nextRequest) => {
|
||||
this.nextRequest = nextRequest;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_interpolate = (str) => {
|
||||
|
@ -58,6 +58,7 @@ chai.use(function (chai, utils) {
|
||||
* endsWith : ends with
|
||||
* between : between
|
||||
* isEmpty : is empty
|
||||
* isNotEmpty : is not empty
|
||||
* isNull : is null
|
||||
* isUndefined : is undefined
|
||||
* isDefined : is defined
|
||||
@ -95,6 +96,7 @@ const parseAssertionOperator = (str = '') => {
|
||||
'endsWith',
|
||||
'between',
|
||||
'isEmpty',
|
||||
'isNotEmpty',
|
||||
'isNull',
|
||||
'isUndefined',
|
||||
'isDefined',
|
||||
@ -109,6 +111,7 @@ const parseAssertionOperator = (str = '') => {
|
||||
|
||||
const unaryOperators = [
|
||||
'isEmpty',
|
||||
'isNotEmpty',
|
||||
'isNull',
|
||||
'isUndefined',
|
||||
'isDefined',
|
||||
@ -147,6 +150,7 @@ const parseAssertionOperator = (str = '') => {
|
||||
const isUnaryOperator = (operator) => {
|
||||
const unaryOperators = [
|
||||
'isEmpty',
|
||||
'isNotEmpty',
|
||||
'isNull',
|
||||
'isUndefined',
|
||||
'isDefined',
|
||||
@ -345,6 +349,9 @@ class AssertRuntime {
|
||||
case 'isEmpty':
|
||||
expect(lhs).to.be.empty;
|
||||
break;
|
||||
case 'isNotEmpty':
|
||||
expect(lhs).to.not.be.empty;
|
||||
break;
|
||||
case 'isNull':
|
||||
expect(lhs).to.be.null;
|
||||
break;
|
||||
@ -401,6 +408,8 @@ class AssertRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
request.assertionResults = assertionResults;
|
||||
|
||||
return assertionResults;
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,8 @@ class ScriptRuntime {
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
) {
|
||||
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
@ -92,6 +93,10 @@ class ScriptRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
if(runRequestByItemPathname) {
|
||||
context.bru.runRequest = runRequestByItemPathname;
|
||||
}
|
||||
|
||||
if (this.runtime === 'quickjs') {
|
||||
await executeQuickJsVmAsync({
|
||||
script: script,
|
||||
@ -104,7 +109,9 @@ class ScriptRuntime {
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
nextRequestName: bru.nextRequest
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
stopExecution: bru.stopExecution
|
||||
};
|
||||
}
|
||||
|
||||
@ -152,7 +159,9 @@ class ScriptRuntime {
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
nextRequestName: bru.nextRequest
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
stopExecution: bru.stopExecution
|
||||
};
|
||||
}
|
||||
|
||||
@ -165,7 +174,8 @@ class ScriptRuntime {
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
) {
|
||||
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
@ -209,6 +219,10 @@ class ScriptRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
if(runRequestByItemPathname) {
|
||||
context.bru.runRequest = runRequestByItemPathname;
|
||||
}
|
||||
|
||||
if (this.runtime === 'quickjs') {
|
||||
await executeQuickJsVmAsync({
|
||||
script: script,
|
||||
@ -221,7 +235,9 @@ class ScriptRuntime {
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
nextRequestName: bru.nextRequest
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
stopExecution: bru.stopExecution
|
||||
};
|
||||
}
|
||||
|
||||
@ -269,7 +285,9 @@ class ScriptRuntime {
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
nextRequestName: bru.nextRequest
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
stopExecution: bru.stopExecution
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user