mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 05:35:41 +00:00
Merge branch 'usebruno:main' into feat/digest-auth-updates
This commit is contained in:
5
.github/workflows/npm-bru-cli.yml
vendored
5
.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
|
||||
|
||||
730
package-lock.json
generated
730
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,20 +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",
|
||||
"nanoid": "3.3.8",
|
||||
"path": "^0.12.7",
|
||||
"pdfjs-dist": "4.4.168",
|
||||
"platform": "^1.3.6",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -90,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)) {
|
||||
@@ -166,7 +166,7 @@ const AssertionRow = ({
|
||||
handleAssertionChange(
|
||||
{
|
||||
target: {
|
||||
value: `${op} ${value}`
|
||||
value: isUnaryOperator(op) ? op : `${op} ${value}`
|
||||
}
|
||||
},
|
||||
assertion,
|
||||
@@ -182,7 +182,7 @@ const AssertionRow = ({
|
||||
theme={storedTheme}
|
||||
readOnly={true}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
onChange={(newValue) => {
|
||||
handleAssertionChange(
|
||||
{
|
||||
target: {
|
||||
@@ -192,6 +192,7 @@ const AssertionRow = ({
|
||||
assertion,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -39,12 +39,15 @@ 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);
|
||||
}
|
||||
|
||||
@@ -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,27 @@ 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.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]);
|
||||
}
|
||||
|
||||
if (e.shiftKey && e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const currentIndex = languages.findIndex((lang) => lang.name === selectedLanguage.name);
|
||||
const nextIndex = (currentIndex - 1 + languages.length) % languages.length;
|
||||
setSelectedLanguage(languages[nextIndex]);
|
||||
}
|
||||
}}
|
||||
aria-pressed={language.name === selectedLanguage.name}
|
||||
>
|
||||
<span className="capitalize">{language.name}</span>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -57,7 +57,7 @@ function getDataString(request) {
|
||||
console.error('Failed to parse JSON data:', error);
|
||||
return { data: request.data.toString() };
|
||||
}
|
||||
} else if (contentType && contentType.includes('application/xml')) {
|
||||
} else if (contentType && (contentType.includes('application/xml') || contentType.includes('text/plain'))) {
|
||||
return { data: request.data };
|
||||
}
|
||||
|
||||
@@ -174,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)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -291,8 +291,9 @@ export const exportCollection = (collection) => {
|
||||
};
|
||||
}
|
||||
default: {
|
||||
console.error('Unsupported auth mode:', itemAuth.mode);
|
||||
return null;
|
||||
return {
|
||||
type: 'noauth'
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -136,8 +136,7 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
if (request.body.mode === 'multipartForm') {
|
||||
axiosRequest.headers['content-type'] = 'multipart/form-data';
|
||||
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
|
||||
const collectionPath = process.cwd();
|
||||
axiosRequest.data = createFormData(enabledParams, collectionPath);
|
||||
axiosRequest.data = enabledParams;
|
||||
}
|
||||
|
||||
if (request.body.mode === 'graphql') {
|
||||
|
||||
@@ -38,10 +38,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 };
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,7 @@
|
||||
"dmg-license": "^1.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "31.2.1",
|
||||
"electron": "33.2.1",
|
||||
"electron-builder": "25.1.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,10 +366,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 };
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -11,7 +11,7 @@ const bundleLibraries = async () => {
|
||||
import moment from "moment";
|
||||
import btoa from "btoa";
|
||||
import atob from "atob";
|
||||
import * as CryptoJS from "crypto-js-3.1.9-1";
|
||||
import * as CryptoJS from "@usebruno/crypto-js";
|
||||
globalThis.expect = expect;
|
||||
globalThis.assert = assert;
|
||||
globalThis.moment = moment;
|
||||
|
||||
@@ -11,6 +11,7 @@ post {
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
foo: {"bar":"baz"} @contentType(application/json--test)
|
||||
form-data-key: {{form-data-key}}
|
||||
form-data-stringified-object: {{form-data-stringified-object}}
|
||||
file: @file(bruno.png)
|
||||
@@ -19,6 +20,7 @@ body:multipart-form {
|
||||
assert {
|
||||
res.body: contains form-data-value
|
||||
res.body: contains {"foo":123}
|
||||
res.body: contains Content-Type: application/json--test
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
meta {
|
||||
name: mixed-content-types
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/multipart/mixed-content-types
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
param1: test
|
||||
param2: {"test":"i am json"} @contentType(application/json)
|
||||
param3: @file(multipart/small.png)
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
res.body.find(p=>p.name === 'param1').contentType: isUndefined
|
||||
res.body.find(p=>p.name === 'param2').contentType: eq application/json
|
||||
res.body.find(p=>p.name === 'param3').contentType: eq image/png
|
||||
}
|
||||
Reference in New Issue
Block a user