mirror of
https://github.com/usebruno/bruno.git
synced 2024-12-22 06:31:18 +01:00
Merge branch 'usebruno:main' into feature/add-raw-file-request-body-option
This commit is contained in:
commit
77879913ff
5
.github/workflows/npm-bru-cli.yml
vendored
5
.github/workflows/npm-bru-cli.yml
vendored
@ -2,11 +2,6 @@ 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
|
||||
|
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@ -5,6 +5,9 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
name: Unit Tests
|
||||
|
38
package-lock.json
generated
38
package-lock.json
generated
@ -9515,7 +9515,6 @@
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -13912,6 +13911,12 @@
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/load-script": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
|
||||
"integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loader-runner": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
|
||||
@ -14426,6 +14431,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
@ -17154,6 +17165,28 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-player": {
|
||||
"version": "2.16.0",
|
||||
"resolved": "https://registry.npmjs.org/react-player/-/react-player-2.16.0.tgz",
|
||||
"integrity": "sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.0.0",
|
||||
"load-script": "^1.0.0",
|
||||
"memoize-one": "^5.1.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-fast-compare": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-player/node_modules/react-fast-compare": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "7.2.9",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
|
||||
@ -20575,6 +20608,7 @@
|
||||
"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-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
@ -20688,7 +20722,7 @@
|
||||
},
|
||||
"packages/bruno-electron": {
|
||||
"name": "bruno",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.34.2",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.658.1",
|
||||
"@usebruno/common": "0.1.0",
|
||||
|
2
packages/bruno-app/.gitignore
vendored
2
packages/bruno-app/.gitignore
vendored
@ -32,3 +32,5 @@ yarn-error.log*
|
||||
# next.js
|
||||
.next/
|
||||
out/
|
||||
|
||||
.env
|
16
packages/bruno-app/jest.config.js
Normal file
16
packages/bruno-app/jest.config.js
Normal file
@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
rootDir: '.',
|
||||
moduleNameMapper: {
|
||||
'^assets/(.*)$': '<rootDir>/src/assets/$1',
|
||||
'^components/(.*)$': '<rootDir>/src/components/$1',
|
||||
'^hooks/(.*)$': '<rootDir>/src/hooks/$1',
|
||||
'^themes/(.*)$': '<rootDir>/src/themes/$1',
|
||||
'^api/(.*)$': '<rootDir>/src/api/$1',
|
||||
'^pageComponents/(.*)$': '<rootDir>/src/pageComponents/$1',
|
||||
'^providers/(.*)$': '<rootDir>/src/providers/$1',
|
||||
'^utils/(.*)$': '<rootDir>/src/utils/$1'
|
||||
},
|
||||
clearMocks: true,
|
||||
moduleDirectories: ['node_modules', 'src'],
|
||||
testEnvironment: 'node'
|
||||
};
|
@ -66,6 +66,7 @@
|
||||
"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-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
|
@ -26,6 +26,12 @@ const StyledWrapper = styled.div`
|
||||
|
||||
.CodeMirror-dialog {
|
||||
overflow: visible;
|
||||
input {
|
||||
background: transparent;
|
||||
border: 1px solid #d3d6db;
|
||||
outline: none;
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
#search-results-count {
|
||||
@ -82,6 +88,14 @@ const StyledWrapper = styled.div`
|
||||
.CodeMirror-search-hint {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.cm-s-default span.cm-property {
|
||||
color: #1f61a0 !important;
|
||||
}
|
||||
|
||||
.cm-s-default span.cm-variable {
|
||||
color: #397d13 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
@ -15,7 +15,7 @@ import { JSHINT } from 'jshint';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const TAB_SIZE = 2;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
@ -58,13 +58,14 @@ if (!SERVER_RENDERED) {
|
||||
'req.getExecutionMode()',
|
||||
'bru',
|
||||
'bru.cwd()',
|
||||
'bru.getEnvName(key)',
|
||||
'bru.getEnvName()',
|
||||
'bru.getProcessEnv(key)',
|
||||
'bru.hasEnvVar(key)',
|
||||
'bru.getEnvVar(key)',
|
||||
'bru.getFolderVar(key)',
|
||||
'bru.getCollectionVar(key)',
|
||||
'bru.setEnvVar(key,value)',
|
||||
'bru.deleteEnvVar(key)',
|
||||
'bru.hasVar(key)',
|
||||
'bru.getVar(key)',
|
||||
'bru.setVar(key,value)',
|
||||
@ -189,32 +190,8 @@ export default class CodeEditor extends React.Component {
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
'Cmd-I': 'unfoldAll',
|
||||
'Cmd-/': (cm) => {
|
||||
// comment/uncomment every selected line(s)
|
||||
const selections = cm.listSelections();
|
||||
selections.forEach((range) => {
|
||||
for (let i = range.from().line; i <= range.to().line; i++) {
|
||||
const selectedLine = cm.getLine(i);
|
||||
// if commented line, remove comment
|
||||
if (selectedLine.trim().startsWith('//')) {
|
||||
cm.replaceRange(
|
||||
selectedLine.replace(/^(\s*)\/\/\s?/, '$1'),
|
||||
{ line: i, ch: 0 },
|
||||
{ line: i, ch: selectedLine.length }
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// otherwise add comment
|
||||
cm.replaceRange(
|
||||
selectedLine.search(/\S|$/) >= TAB_SIZE
|
||||
? ' '.repeat(TAB_SIZE) + '// ' + selectedLine.trim()
|
||||
: '// ' + selectedLine,
|
||||
{ line: i, ch: 0 },
|
||||
{ line: i, ch: selectedLine.length }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
'Ctrl-/': 'toggleComment',
|
||||
'Cmd-/': 'toggleComment'
|
||||
},
|
||||
foldOptions: {
|
||||
widget: (from, to) => {
|
||||
@ -281,9 +258,9 @@ export default class CodeEditor extends React.Component {
|
||||
while (end < currentLine.length && /[^{}();\s\[\]\,]/.test(currentLine.charAt(end))) ++end;
|
||||
while (start && /[^{}();\s\[\]\,]/.test(currentLine.charAt(start - 1))) --start;
|
||||
let curWord = start != end && currentLine.slice(start, end);
|
||||
//Qualify if autocomplete will be shown
|
||||
// Qualify if autocomplete will be shown
|
||||
if (
|
||||
/^(?!Shift|Tab|Enter|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|\s)\w*/.test(event.key) &&
|
||||
/^(?!Shift|Tab|Enter|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Meta|Alt|Home|End\s)\w*/.test(event.key) &&
|
||||
curWord.length > 0 &&
|
||||
!/\/\/|\/\*|.*{{|`[^$]*{|`[^{]*$/.test(currentLine.slice(0, end)) &&
|
||||
/(?<!\d)[a-zA-Z\._]$/.test(curWord)
|
||||
|
@ -5,7 +5,7 @@ import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
|
@ -54,6 +54,14 @@ const StyledWrapper = styled.div`
|
||||
.CodeMirror-search-hint {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.cm-s-default span.cm-property {
|
||||
color: #1f61a0 !important;
|
||||
}
|
||||
|
||||
.cm-s-default span.cm-variable {
|
||||
color: #397d13 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
@ -19,7 +19,7 @@ import { IconWand } from '@tabler/icons';
|
||||
import onHasCompletion from './onHasCompletion';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
|
@ -1,14 +1,41 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import { get } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { Document, Page } from 'react-pdf';
|
||||
import { useState } from 'react';
|
||||
import 'pdfjs-dist/build/pdf.worker';
|
||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
import 'react-pdf/dist/esm/Page/TextLayer.css';
|
||||
import { GlobalWorkerOptions } from 'pdfjs-dist/build/pdf';
|
||||
GlobalWorkerOptions.workerSrc = 'pdfjs-dist/legacy/build/pdf.worker.min.mjs';
|
||||
import ReactPlayer from 'react-player';
|
||||
|
||||
const VideoPreview = React.memo(({ contentType, dataBuffer }) => {
|
||||
const [videoUrl, setVideoUrl] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const videoType = contentType.split(';')[0];
|
||||
const byteArray = Buffer.from(dataBuffer, 'base64');
|
||||
const blob = new Blob([byteArray], { type: videoType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
setVideoUrl(url);
|
||||
return () => URL.revokeObjectURL(url);
|
||||
}, [contentType, dataBuffer]);
|
||||
|
||||
if (!videoUrl) return <div>Loading video...</div>;
|
||||
|
||||
return (
|
||||
<ReactPlayer
|
||||
url={videoUrl}
|
||||
controls
|
||||
muted={true}
|
||||
width="100%"
|
||||
height="100%"
|
||||
onError={(e) => console.error('Error loading video:', e)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const QueryResultPreview = ({
|
||||
previewTab,
|
||||
@ -73,9 +100,7 @@ const QueryResultPreview = ({
|
||||
);
|
||||
}
|
||||
case 'preview-video': {
|
||||
return (
|
||||
<video controls src={`data:${contentType.replace(/\;(.*)/, '')};base64,${dataBuffer}`} className="mx-auto" />
|
||||
);
|
||||
return <VideoPreview contentType={contentType} dataBuffer={dataBuffer} />;
|
||||
}
|
||||
default:
|
||||
case 'raw': {
|
||||
|
@ -8,7 +8,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
|
||||
let posthogClient = null;
|
||||
const posthogApiKey = 'phc_7gtqSrrdZRohiozPMLIacjzgHbUlhalW1Bu16uYijMR';
|
||||
const posthogApiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY;
|
||||
const getPosthogClient = () => {
|
||||
if (posthogClient) {
|
||||
return posthogClient;
|
||||
|
@ -39,6 +39,14 @@ const StyledWrapper = styled.div`
|
||||
textarea.curl-command {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useRef, useEffect, useCallback } from 'react';
|
||||
import React, { useRef, useEffect, useCallback, forwardRef, useState } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
@ -12,6 +12,8 @@ import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelect
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { getRequestFromCurlCommand } from 'utils/curl';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
|
||||
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -19,6 +21,39 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
const {
|
||||
brunoConfig: { presets: collectionPresets = {} }
|
||||
} = collection;
|
||||
const [curlRequestTypeDetected, setCurlRequestTypeDetected] = useState(null);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end auth-type-label select-none">
|
||||
{curlRequestTypeDetected === 'http-request' ? "HTTP" : "GraphQL"}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// This function analyzes a given cURL command string and determines whether the request is a GraphQL or HTTP request.
|
||||
const identifyCurlRequestType = (url, headers, body) => {
|
||||
if (url.endsWith('/graphql')) {
|
||||
setCurlRequestTypeDetected('graphql-request');
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value;
|
||||
if (contentType && contentType.includes('application/graphql')) {
|
||||
setCurlRequestTypeDetected('graphql-request');
|
||||
return;
|
||||
}
|
||||
|
||||
setCurlRequestTypeDetected('http-request');
|
||||
};
|
||||
|
||||
const curlRequestTypeChange = (type) => {
|
||||
setCurlRequestTypeDetected(type);
|
||||
};
|
||||
|
||||
const getRequestType = (collectionPresets) => {
|
||||
if (!collectionPresets || !collectionPresets.requestType) {
|
||||
@ -99,11 +134,11 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
} else if (values.requestType === 'from-curl') {
|
||||
const request = getRequestFromCurlCommand(values.curlCommand);
|
||||
const request = getRequestFromCurlCommand(values.curlCommand, curlRequestTypeDetected);
|
||||
dispatch(
|
||||
newHttpRequest({
|
||||
requestName: values.requestName,
|
||||
requestType: 'http-request',
|
||||
requestType: curlRequestTypeDetected,
|
||||
requestUrl: request.url,
|
||||
requestMethod: request.method,
|
||||
collectionUid: collection.uid,
|
||||
@ -158,6 +193,12 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
formik.setFieldValue('requestType', 'from-curl');
|
||||
formik.setFieldValue('curlCommand', pastedData);
|
||||
|
||||
// Identify the request type
|
||||
const request = getRequestFromCurlCommand(pastedData);
|
||||
if (request) {
|
||||
identifyCurlRequestType(request.url, request.headers, request.body);
|
||||
}
|
||||
|
||||
// Prevent the default paste behavior to avoid pasting into the textarea
|
||||
event.preventDefault();
|
||||
}
|
||||
@ -165,6 +206,18 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
[formik]
|
||||
);
|
||||
|
||||
const handleCurlCommandChange = (event) => {
|
||||
formik.handleChange(event);
|
||||
|
||||
if (event.target.name === 'curlCommand') {
|
||||
const curlCommand = event.target.value;
|
||||
const request = getRequestFromCurlCommand(curlCommand);
|
||||
if (request) {
|
||||
identifyCurlRequestType(request.url, request.headers, request.body);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
@ -279,15 +332,37 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-4">
|
||||
<label htmlFor="request-url" className="block font-semibold">
|
||||
cURL Command
|
||||
</label>
|
||||
<div className="flex justify-between">
|
||||
<label htmlFor="request-url" className="block font-semibold">
|
||||
cURL Command
|
||||
</label>
|
||||
<Dropdown className="dropdown" onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
curlRequestTypeChange('http-request');
|
||||
}}
|
||||
>
|
||||
HTTP
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
curlRequestTypeChange('graphql-request');
|
||||
}}
|
||||
>
|
||||
GraphQL
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<textarea
|
||||
name="curlCommand"
|
||||
placeholder="Enter cURL request here.."
|
||||
className="block textbox w-full mt-4 curl-command"
|
||||
value={formik.values.curlCommand}
|
||||
onChange={formik.handleChange}
|
||||
onChange={handleCurlCommandChange}
|
||||
></textarea>
|
||||
{formik.touched.curlCommand && formik.errors.curlCommand ? (
|
||||
<div className="text-red-500">{formik.errors.curlCommand}</div>
|
||||
|
@ -6,7 +6,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
|
@ -233,7 +233,7 @@ const GlobalStyle = createGlobalStyle`
|
||||
}
|
||||
|
||||
.CodeMirror-hint-active {
|
||||
background: #89f !important;
|
||||
background: #08f !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
`;
|
||||
|
@ -10,7 +10,7 @@ import 'codemirror/theme/material.css';
|
||||
import 'codemirror/theme/monokai.css';
|
||||
import 'codemirror/addon/scroll/simplescrollbars.css';
|
||||
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
if (!SERVER_RENDERED) {
|
||||
require('codemirror/mode/javascript/javascript');
|
||||
require('codemirror/mode/xml/xml');
|
||||
|
@ -8,7 +8,6 @@ import ReduxStore from 'providers/ReduxStore';
|
||||
import ThemeProvider from 'providers/Theme/index';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
|
||||
import '../styles/app.scss';
|
||||
import '../styles/globals.css';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'graphiql/graphiql.min.css';
|
||||
@ -31,7 +30,7 @@ function SafeHydrate({ children }) {
|
||||
}
|
||||
|
||||
function NoSsr({ children }) {
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined';
|
||||
const SERVER_RENDERED = typeof window === 'undefined';
|
||||
|
||||
if (SERVER_RENDERED) {
|
||||
return null;
|
||||
|
@ -13,7 +13,7 @@ import platformLib from 'platform';
|
||||
import { uuid } from 'utils/common';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const posthogApiKey = 'phc_7gtqSrrdZRohiozPMLIacjzgHbUlhalW1Bu16uYijMR';
|
||||
const posthogApiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY;
|
||||
let posthogClient = null;
|
||||
|
||||
const isPlaywrightTestRunning = () => {
|
||||
|
@ -92,9 +92,7 @@ export const addGlobalEnvironment = ({ name, variables = [] }) => (dispatch, get
|
||||
const uid = uuid();
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-global-environment', { name, uid, variables })
|
||||
.then(
|
||||
dispatch(_addGlobalEnvironment({ name, uid, variables }))
|
||||
)
|
||||
.then(() => dispatch(_addGlobalEnvironment({ name, uid, variables })))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
@ -108,9 +106,7 @@ export const copyGlobalEnvironment = ({ name, environmentUid: baseEnvUid }) => (
|
||||
const uid = uuid();
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-global-environment', { uid, name, variables: baseEnv.variables })
|
||||
.then(() => {
|
||||
dispatch(_copyGlobalEnvironment({ name, uid, variables: baseEnv.variables }))
|
||||
})
|
||||
.then(() => dispatch(_copyGlobalEnvironment({ name, uid, variables: baseEnv.variables })))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
@ -127,9 +123,7 @@ export const renameGlobalEnvironment = ({ name: newName, environmentUid }) => (d
|
||||
environmentSchema
|
||||
.validate(environment)
|
||||
.then(() => ipcRenderer.invoke('renderer:rename-global-environment', { name: newName, environmentUid }))
|
||||
.then(
|
||||
dispatch(_renameGlobalEnvironment({ name: newName, environmentUid }))
|
||||
)
|
||||
.then(() => dispatch(_renameGlobalEnvironment({ name: newName, environmentUid })))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
@ -151,9 +145,7 @@ export const saveGlobalEnvironment = ({ variables, environmentUid }) => (dispatc
|
||||
environmentUid,
|
||||
variables
|
||||
}))
|
||||
.then(
|
||||
dispatch(_saveGlobalEnvironment({ environmentUid, variables }))
|
||||
)
|
||||
.then(() => dispatch(_saveGlobalEnvironment({ environmentUid, variables })))
|
||||
.then(resolve)
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
@ -165,9 +157,7 @@ export const selectGlobalEnvironment = ({ environmentUid }) => (dispatch, getSta
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:select-global-environment', { environmentUid })
|
||||
.then(
|
||||
dispatch(_selectGlobalEnvironment({ environmentUid }))
|
||||
)
|
||||
.then(() => dispatch(_selectGlobalEnvironment({ environmentUid })))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
@ -177,9 +167,7 @@ export const deleteGlobalEnvironment = ({ environmentUid }) => (dispatch, getSta
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:delete-global-environment', { environmentUid })
|
||||
.then(
|
||||
dispatch(_deleteGlobalEnvironment({ environmentUid }))
|
||||
)
|
||||
.then(() => dispatch(_deleteGlobalEnvironment({ environmentUid })))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
@ -195,7 +183,6 @@ export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) =>
|
||||
const environment = globalEnvironments?.find(env => env?.uid == environmentUid);
|
||||
|
||||
if (!environment || !environmentUid) {
|
||||
console.error('Global Environment not found');
|
||||
return resolve();
|
||||
}
|
||||
|
||||
@ -228,9 +215,7 @@ export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) =>
|
||||
environmentUid,
|
||||
variables
|
||||
}))
|
||||
.then(
|
||||
dispatch(_saveGlobalEnvironment({ environmentUid, variables }))
|
||||
)
|
||||
.then(() => dispatch(_saveGlobalEnvironment({ environmentUid, variables })))
|
||||
.then(resolve)
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
|
@ -23,18 +23,19 @@
|
||||
--color-method-options: rgb(52 52 52);
|
||||
--color-method-head: rgb(52 52 52);
|
||||
}
|
||||
|
||||
:root,.graphiql-container,.CodeMirror-info,.CodeMirror-lint-tooltip,reach-portal {
|
||||
/* Required CSS variables after upgrading GraphiQL from v1.5.9 to v2.4.7 */
|
||||
/* Colors */
|
||||
--color-primary: 320, 95%, 43% !important;
|
||||
--color-secondary: 242, 51%, 61% !important;
|
||||
--color-tertiary: 188, 100%, 36% !important;
|
||||
--color-info: 208, 100%, 46% !important;
|
||||
--color-success: 158, 60%, 42% !important;
|
||||
--color-warning: 36, 100%, 41% !important;
|
||||
--color-error: 13, 93%, 58% !important;
|
||||
--color-neutral: 219, 28%, 32% !important;
|
||||
--color-base: 219, 28%, 100% !important;
|
||||
--color-primary: 0, 0%, 0% !important;
|
||||
--color-secondary: 0, 0%, 0% !important;
|
||||
--color-tertiary: 0, 0%, 0% !important;
|
||||
--color-info: 0, 0%, 0% !important;
|
||||
--color-success: 0, 0%, 0% !important;
|
||||
--color-warning: 0, 0%, 0% !important;
|
||||
--color-error: 0, 0%, 0% !important;
|
||||
--color-neutral: 0, 0%, 0% !important;
|
||||
--color-base: 0, 0%, 100% !important;
|
||||
|
||||
/* Color alpha values */
|
||||
--alpha-secondary: 0.76 !important;
|
||||
@ -43,6 +44,59 @@
|
||||
--alpha-background-medium: 0.1 !important;
|
||||
--alpha-background-light: 0.07 !important;
|
||||
|
||||
--font-family: Consolas,Inconsolata,Droid Sans Mono,Monaco,monospace;
|
||||
--font-family-mono: 'Fira Code', monospace;
|
||||
--font-size-hint: .75rem;
|
||||
--font-size-inline-code: .8125rem;
|
||||
--font-size-body: .8rem;
|
||||
--font-size-h4: 1.125rem;
|
||||
--font-size-h3: 1.375rem;
|
||||
--font-size-h2: 1.8125rem;
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--line-height: 1.5;
|
||||
--px-2: 0px;
|
||||
--px-4: 0px;
|
||||
--px-6: 2px;
|
||||
--px-8: 8px;
|
||||
--px-10: 10px;
|
||||
--px-12: 12px;
|
||||
--px-16: 16px;
|
||||
--px-20: 20px;
|
||||
--px-24: 24px;
|
||||
--border-radius-2: 0px !important;
|
||||
--border-radius-4: 0px !important;
|
||||
--border-radius-8: 0px !important;
|
||||
--border-radius-12: 0px !important;
|
||||
--popover-box-shadow: 0px 0px 1px #000 !important;
|
||||
--popover-border: none;
|
||||
--sidebar-width: 60px;
|
||||
--toolbar-width: 40px;
|
||||
--session-header-height: 51px
|
||||
}
|
||||
|
||||
/* Required CSS variables after upgrading GraphiQL from v1.5.9 to v2.4.7 */
|
||||
.graphiql-container, .CodeMirror-info, .CodeMirror-lint-tooltip, reach-portal {
|
||||
/* General Colors */
|
||||
--color-primary: 0, 0%, 0% !important;
|
||||
--color-secondary: 0, 0%, 0% !important;
|
||||
--color-tertiary: 0, 0%, 0% !important;
|
||||
--color-info: 0, 0%, 0% !important;
|
||||
--color-success: 0, 0%, 0% !important;
|
||||
--color-warning: 0, 0%, 0% !important;
|
||||
--color-error: 0, 0%, 0% !important;
|
||||
--color-base: 0, 0%, 100% !important;
|
||||
--color-neutral: 0, 0%, 60% !important;
|
||||
|
||||
/* Color alpha values */
|
||||
--alpha-secondary: 0.76 !important;
|
||||
--alpha-tertiary: 0.5 !important;
|
||||
--alpha-background-heavy: 0.15 !important;
|
||||
--alpha-background-medium: 0.1 !important;
|
||||
--alpha-background-light: 0.07 !important;
|
||||
|
||||
--font-family: Consolas,Inconsolata,Droid Sans Mono,Monaco,monospace;
|
||||
--font-family-mono: 'Fira Code', monospace;
|
||||
--font-size-hint: .75rem;
|
||||
--font-size-inline-code: .8125rem;
|
||||
--font-size-body: .9375rem;
|
||||
@ -52,15 +106,15 @@
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--line-height: 1.5;
|
||||
--px-2: 2px;
|
||||
--px-4: 4px;
|
||||
--px-6: 6px;
|
||||
--px-8: 8px;
|
||||
--px-10: 10px;
|
||||
--px-12: 12px;
|
||||
--px-16: 16px;
|
||||
--px-20: 20px;
|
||||
--px-24: 24px;
|
||||
--px-2: 2px !important;
|
||||
--px-4: 4px !important;
|
||||
--px-6: 6px !important;
|
||||
--px-8: 8px !important;
|
||||
--px-10: 10px !important;
|
||||
--px-12: 12px !important;
|
||||
--px-16: 16px !important;
|
||||
--px-20: 20px !important;
|
||||
--px-24: 24px !important;
|
||||
--border-radius-2: 2px !important;
|
||||
--border-radius-4: 2px !important;
|
||||
--border-radius-8: 2px !important;
|
||||
@ -72,6 +126,15 @@
|
||||
--session-header-height: 51px
|
||||
}
|
||||
|
||||
.CodeMirror-dialog {
|
||||
--px-4: 0px !important;
|
||||
--px-12: 2px !important;
|
||||
}
|
||||
|
||||
.graphiql-container {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
|
@ -19,16 +19,23 @@ const createContentType = (mode) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a list of enabled headers for the request, ensuring no duplicate content-type headers.
|
||||
*
|
||||
* @param {Object} request - The request object.
|
||||
* @param {Object[]} headers - The array of header objects, each containing name, value, and enabled properties.
|
||||
* @returns {Object[]} - An array of enabled headers with normalized names and values.
|
||||
*/
|
||||
const createHeaders = (request, headers) => {
|
||||
const enabledHeaders = headers
|
||||
.filter((header) => header.enabled)
|
||||
.map((header) => ({
|
||||
name: header.name,
|
||||
name: header.name.toLowerCase(),
|
||||
value: header.value
|
||||
}));
|
||||
|
||||
const contentType = createContentType(request.body?.mode);
|
||||
if (contentType !== '') {
|
||||
if (contentType !== '' && !enabledHeaders.some((header) => header.name === 'content-type')) {
|
||||
enabledHeaders.push({ name: 'content-type', value: contentType });
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
|
@ -12,7 +12,7 @@ import brunoCommon from '@usebruno/common';
|
||||
const { interpolate } = brunoCommon;
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const { get } = require('lodash');
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
@ -44,8 +44,11 @@ if (!SERVER_RENDERED) {
|
||||
const into = document.createElement('div');
|
||||
const descriptionDiv = document.createElement('div');
|
||||
descriptionDiv.className = 'info-description';
|
||||
|
||||
descriptionDiv.appendChild(document.createTextNode(variableValue));
|
||||
if (options?.variables?.maskedEnvVariables?.includes(variableName)) {
|
||||
descriptionDiv.appendChild(document.createTextNode('*****'));
|
||||
} else {
|
||||
descriptionDiv.appendChild(document.createTextNode(variableValue));
|
||||
}
|
||||
into.appendChild(descriptionDiv);
|
||||
|
||||
return into;
|
||||
|
@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
|
@ -820,6 +820,23 @@ export const getEnvironmentVariables = (collection) => {
|
||||
return variables;
|
||||
};
|
||||
|
||||
export const getEnvironmentVariablesMasked = (collection) => {
|
||||
// Return an empty array if the collection is invalid or not provided
|
||||
if (!collection || !collection.activeEnvironmentUid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Find the active environment in the collection
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
if (!environment || !environment.variables) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter the environment variables to get only the masked (secret) ones
|
||||
return environment.variables
|
||||
.filter((variable) => variable.name && variable.value && variable.enabled && variable.secret)
|
||||
.map((variable) => variable.name);
|
||||
};
|
||||
|
||||
const getPathParams = (item) => {
|
||||
let pathParams = {};
|
||||
@ -855,6 +872,13 @@ export const getAllVariables = (collection, item) => {
|
||||
const { globalEnvironmentVariables = {} } = collection;
|
||||
|
||||
const { processEnvVariables = {}, runtimeVariables = {} } = collection;
|
||||
const mergedVariables = {
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...runtimeVariables
|
||||
};
|
||||
const maskedEnvVariables = getEnvironmentVariablesMasked(collection);
|
||||
const filteredMaskedEnvVariables = maskedEnvVariables.filter((key) => !(key in mergedVariables));
|
||||
|
||||
return {
|
||||
...globalEnvironmentVariables,
|
||||
@ -866,6 +890,7 @@ export const getAllVariables = (collection, item) => {
|
||||
pathParams: {
|
||||
...pathParams
|
||||
},
|
||||
maskedEnvVariables: filteredMaskedEnvVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVariables
|
||||
|
@ -1,7 +1,7 @@
|
||||
import get from 'lodash/get';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
@ -52,17 +52,33 @@ export class MaskedEditor {
|
||||
/** Replaces all rendered characters, with the provided character. */
|
||||
maskContent = () => {
|
||||
const content = this.editor.getValue();
|
||||
const lineCount = this.editor.lineCount();
|
||||
|
||||
if (lineCount === 0) return;
|
||||
this.editor.operation(() => {
|
||||
// Clear previous masked text
|
||||
this.editor.getAllMarks().forEach((mark) => mark.clear());
|
||||
// Apply new masked text
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
if (content[i] !== '\n') {
|
||||
const maskedNode = document.createTextNode(this.maskChar);
|
||||
|
||||
if (content.length <= 500) {
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
if (content[i] !== '\n') {
|
||||
const maskedNode = document.createTextNode(this.maskChar);
|
||||
this.editor.markText(
|
||||
{ line: this.editor.posFromIndex(i).line, ch: this.editor.posFromIndex(i).ch },
|
||||
{ line: this.editor.posFromIndex(i + 1).line, ch: this.editor.posFromIndex(i + 1).ch },
|
||||
{ replacedWith: maskedNode, handleMouseEvents: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let line = 0; line < lineCount; line++) {
|
||||
const lineLength = this.editor.getLine(line).length;
|
||||
const maskedNode = document.createTextNode('*'.repeat(lineLength));
|
||||
this.editor.markText(
|
||||
{ line: this.editor.posFromIndex(i).line, ch: this.editor.posFromIndex(i).ch },
|
||||
{ line: this.editor.posFromIndex(i + 1).line, ch: this.editor.posFromIndex(i + 1).ch },
|
||||
{ replacedWith: maskedNode, handleMouseEvents: true }
|
||||
{ line, ch: 0 },
|
||||
{ line, ch: lineLength },
|
||||
{ replacedWith: maskedNode, handleMouseEvents: false }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,12 @@ function getQueries(request) {
|
||||
return queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts request data to a string based on its content type.
|
||||
*
|
||||
* @param {Object} request - The request object containing data and headers.
|
||||
* @returns {Object} An object containing the data string.
|
||||
*/
|
||||
function getDataString(request) {
|
||||
if (typeof request.data === 'number') {
|
||||
request.data = request.data.toString();
|
||||
@ -44,7 +50,13 @@ function getDataString(request) {
|
||||
const contentType = getContentType(request.headers);
|
||||
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return { data: request.data.toString() };
|
||||
try {
|
||||
const parsedData = JSON.parse(request.data);
|
||||
return { data: JSON.stringify(parsedData) };
|
||||
} catch (error) {
|
||||
console.error('Failed to parse JSON data:', error);
|
||||
return { data: request.data.toString() };
|
||||
}
|
||||
}
|
||||
|
||||
const parsedQueryString = querystring.parse(request.data, { sort: false });
|
||||
|
@ -2,7 +2,7 @@ import { forOwn } from 'lodash';
|
||||
import { convertToCodeMirrorJson } from 'utils/common';
|
||||
import curlToJson from './curl-to-json';
|
||||
|
||||
export const getRequestFromCurlCommand = (curlCommand) => {
|
||||
export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-request') => {
|
||||
const parseFormData = (parsedBody) => {
|
||||
const formData = [];
|
||||
forOwn(parsedBody, (value, key) => {
|
||||
@ -12,6 +12,22 @@ export const getRequestFromCurlCommand = (curlCommand) => {
|
||||
return formData;
|
||||
};
|
||||
|
||||
const parseGraphQL = (text) => {
|
||||
try {
|
||||
const graphql = JSON.parse(text);
|
||||
|
||||
return {
|
||||
query: graphql.query,
|
||||
variables: JSON.stringify(graphql.variables, null, 2)
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
query: '',
|
||||
variables: ''
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (!curlCommand || typeof curlCommand !== 'string' || curlCommand.length === 0) {
|
||||
return null;
|
||||
@ -24,6 +40,8 @@ export const getRequestFromCurlCommand = (curlCommand) => {
|
||||
Object.keys(parsedHeaders).map((key) => ({ name: key, value: parsedHeaders[key], enabled: true }));
|
||||
|
||||
const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value;
|
||||
const parsedBody = request.data;
|
||||
|
||||
const body = {
|
||||
mode: 'none',
|
||||
json: null,
|
||||
@ -31,11 +49,15 @@ export const getRequestFromCurlCommand = (curlCommand) => {
|
||||
xml: null,
|
||||
sparql: null,
|
||||
multipartForm: null,
|
||||
formUrlEncoded: null
|
||||
formUrlEncoded: null,
|
||||
graphql: null
|
||||
};
|
||||
const parsedBody = request.data;
|
||||
|
||||
if (parsedBody && contentType && typeof contentType === 'string') {
|
||||
if (contentType.includes('application/json')) {
|
||||
if (requestType === 'graphql-request' && (contentType.includes('application/json') || contentType.includes('application/graphql'))) {
|
||||
body.mode = 'graphql';
|
||||
body.graphql = parseGraphQL(parsedBody);
|
||||
} else if (contentType.includes('application/json')) {
|
||||
body.mode = 'json';
|
||||
body.json = convertToCodeMirrorJson(parsedBody);
|
||||
} else if (contentType.includes('text/xml')) {
|
||||
|
@ -271,7 +271,7 @@ const resolveRefs = (spec, components = spec?.components, visitedItems = new Set
|
||||
|
||||
// Recursively resolve references in nested objects
|
||||
for (const prop in spec) {
|
||||
spec[prop] = resolveRefs(spec[prop], components, visitedItems);
|
||||
spec[prop] = resolveRefs(spec[prop], components, new Set(visitedItems));
|
||||
}
|
||||
|
||||
return spec;
|
||||
@ -316,7 +316,7 @@ const getDefaultUrl = (serverObject) => {
|
||||
url = url.replace(`{${variableName}}`, sub);
|
||||
});
|
||||
}
|
||||
return url.endsWith('/') ? url : `${url}/`;
|
||||
return url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
};
|
||||
|
||||
const getSecurity = (apiSpec) => {
|
||||
@ -353,7 +353,7 @@ const openAPIRuntimeExpressionToScript = (expression) => {
|
||||
return expression;
|
||||
};
|
||||
|
||||
const parseOpenApiCollection = (data) => {
|
||||
export const parseOpenApiCollection = (data) => {
|
||||
const brunoCollection = {
|
||||
name: '',
|
||||
uid: uuid(),
|
||||
|
@ -0,0 +1,67 @@
|
||||
import { parseOpenApiCollection } from './openapi-collection';
|
||||
import { uuid } from 'utils/common';
|
||||
|
||||
jest.mock('utils/common');
|
||||
|
||||
describe('openapi importer util functions', () => {
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
it('should convert openapi object to bruno collection correctly', async () => {
|
||||
const input = {
|
||||
openapi: '3.0.3',
|
||||
info: {
|
||||
title: 'Sample API with Multiple Servers',
|
||||
description: 'API spec with multiple servers.',
|
||||
version: '1.0.0'
|
||||
},
|
||||
servers: [
|
||||
{ url: 'https://api.example.com/v1', description: 'Production Server' },
|
||||
{ url: 'https://staging-api.example.com/v1', description: 'Staging Server' },
|
||||
{ url: 'http://localhost:3000/v1', description: 'Local Server' }
|
||||
],
|
||||
paths: {
|
||||
'/users': {
|
||||
get: {
|
||||
summary: 'Get a list of users',
|
||||
parameters: [
|
||||
{ name: 'page', in: 'query', required: false, schema: { type: 'integer' } },
|
||||
{ name: 'limit', in: 'query', required: false, schema: { type: 'integer' } }
|
||||
],
|
||||
responses: {
|
||||
'200': { description: 'A list of users' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
name: 'Sample API with Multiple Servers',
|
||||
version: '1',
|
||||
items: [
|
||||
{
|
||||
name: 'Get a list of users',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: '{{baseUrl}}/users',
|
||||
method: 'GET',
|
||||
params: [
|
||||
{ name: 'page', value: '', enabled: false, type: 'query' },
|
||||
{ name: 'limit', value: '', enabled: false, type: 'query' }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
environments: [
|
||||
{ name: 'Production Server', variables: [{ name: 'baseUrl', value: 'https://api.example.com/v1' }] },
|
||||
{ name: 'Staging Server', variables: [{ name: 'baseUrl', value: 'https://staging-api.example.com/v1' }] },
|
||||
{ name: 'Local Server', variables: [{ name: 'baseUrl', value: 'http://localhost:3000/v1' }] }
|
||||
]
|
||||
};
|
||||
|
||||
const result = await parseOpenApiCollection(input);
|
||||
|
||||
expect(result).toMatchObject(expectedOutput);
|
||||
expect(uuid).toHaveBeenCalledTimes(10);
|
||||
});
|
||||
});
|
@ -11,6 +11,9 @@ describe('postmanTranslation function', () => {
|
||||
pm.collectionVariables.set('key', 'value');
|
||||
const data = pm.response.json();
|
||||
pm.expect(pm.environment.has('key')).to.be.true;
|
||||
postman.setEnvironmentVariable('key', 'value');
|
||||
postman.getEnvironmentVariable('key');
|
||||
postman.clearEnvironmentVariable('key');
|
||||
`;
|
||||
const expectedOutput = `
|
||||
bru.getEnvVar('key');
|
||||
@ -21,6 +24,9 @@ describe('postmanTranslation function', () => {
|
||||
bru.setVar('key', 'value');
|
||||
const data = res.getBody();
|
||||
expect(bru.getEnvVar('key') !== undefined && bru.getEnvVar('key') !== null).to.be.true;
|
||||
bru.setEnvVar('key', 'value');
|
||||
bru.getEnvVar('key');
|
||||
bru.deleteEnvVar('key');
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
@ -151,3 +157,13 @@ test('should handle response commands', () => {
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should handle tests object', () => {
|
||||
const inputScript = `
|
||||
tests['Status code is 200'] = responseCode.code === 200;
|
||||
`;
|
||||
const expectedOutput = `
|
||||
test("Status code is 200", function() { expect(Boolean(responseCode.code === 200)).to.be.true; });
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
@ -17,7 +17,13 @@ const replacements = {
|
||||
'pm\\.response\\.code': 'res.getStatus()',
|
||||
'pm\\.response\\.text\\(': 'res.getBody()?.toString(',
|
||||
'pm\\.expect\\.fail\\(': 'expect.fail(',
|
||||
'pm\\.response\\.responseTime': 'res.getResponseTime()'
|
||||
'pm\\.response\\.responseTime': 'res.getResponseTime()',
|
||||
'pm\\.environment\\.name': 'bru.getEnvName()',
|
||||
"tests\\['([^']+)'\\]\\s*=\\s*([^;]+);": 'test("$1", function() { expect(Boolean($2)).to.be.true; });',
|
||||
// deprecated translations
|
||||
'postman\\.setEnvironmentVariable\\(': 'bru.setEnvVar(',
|
||||
'postman\\.getEnvironmentVariable\\(': 'bru.getEnvVar(',
|
||||
'postman\\.clearEnvironmentVariable\\(': 'bru.deleteEnvVar(',
|
||||
};
|
||||
|
||||
const extendedReplacements = Object.keys(replacements).reduce((acc, key) => {
|
||||
|
@ -41,6 +41,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"yargs": "^17.6.2"
|
||||
|
@ -211,6 +211,11 @@ const builder = async (yargs) => {
|
||||
description:
|
||||
'The specified custom CA certificate (--cacert) will be used exclusively and the default truststore is ignored, if this option is specified. Evaluated in combination with "--cacert" only.'
|
||||
})
|
||||
.option('disable-cookies', {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Automatically save and sent cookies with requests'
|
||||
})
|
||||
.option('env', {
|
||||
describe: 'Environment variables',
|
||||
type: 'string'
|
||||
@ -259,10 +264,30 @@ const builder = async (yargs) => {
|
||||
type: 'boolean',
|
||||
description: 'Stop execution after a failure of a request, test, or assertion'
|
||||
})
|
||||
.option('reporter-skip-all-headers', {
|
||||
type: 'boolean',
|
||||
description: 'Omit headers from the reporter output',
|
||||
default: false
|
||||
})
|
||||
.option('reporter-skip-headers', {
|
||||
type: 'array',
|
||||
description: 'Skip specific headers from the reporter output',
|
||||
default: []
|
||||
})
|
||||
.option('client-cert-config', {
|
||||
type: 'string',
|
||||
description: 'Path to the Client certificate config file used for securing the connection in the request'
|
||||
})
|
||||
|
||||
.example('$0 run request.bru', 'Run a request')
|
||||
.example('$0 run request.bru --env local', 'Run a request with the environment set to local')
|
||||
.example('$0 run folder', 'Run all requests in a folder')
|
||||
.example('$0 run folder -r', 'Run all requests in a folder recursively')
|
||||
.example('$0 run --reporter-skip-all-headers', 'Run all requests in a folder recursively with omitted headers from the reporter output')
|
||||
.example(
|
||||
'$0 run --reporter-skip-headers "Authorization"',
|
||||
'Run all requests in a folder recursively with skipped headers from the reporter output'
|
||||
)
|
||||
.example(
|
||||
'$0 run request.bru --env local --env-var secret=xxx',
|
||||
'Run a request with the environment set to local and overwrite the variable secret with value xxx'
|
||||
@ -292,7 +317,8 @@ const builder = async (yargs) => {
|
||||
.example(
|
||||
'$0 run folder --cacert myCustomCA.pem --ignore-truststore',
|
||||
'Use a custom CA certificate exclusively when validating the peers of the requests in the specified folder.'
|
||||
);
|
||||
)
|
||||
.example('$0 run --client-cert-config client-cert-config.json', 'Run a request with Client certificate configurations');
|
||||
};
|
||||
|
||||
const handler = async function (argv) {
|
||||
@ -301,6 +327,7 @@ const handler = async function (argv) {
|
||||
filename,
|
||||
cacert,
|
||||
ignoreTruststore,
|
||||
disableCookies,
|
||||
env,
|
||||
envVar,
|
||||
insecure,
|
||||
@ -312,7 +339,10 @@ const handler = async function (argv) {
|
||||
reporterHtml,
|
||||
sandbox,
|
||||
testsOnly,
|
||||
bail
|
||||
bail,
|
||||
reporterSkipAllHeaders,
|
||||
reporterSkipHeaders,
|
||||
clientCertConfig
|
||||
} = argv;
|
||||
const collectionPath = process.cwd();
|
||||
|
||||
@ -330,6 +360,41 @@ const handler = async function (argv) {
|
||||
const brunoConfig = JSON.parse(brunoConfigFile);
|
||||
const collectionRoot = getCollectionRoot(collectionPath);
|
||||
|
||||
if (clientCertConfig) {
|
||||
try {
|
||||
const clientCertConfigExists = await exists(clientCertConfig);
|
||||
if (!clientCertConfigExists) {
|
||||
console.error(chalk.red(`Client Certificate Config file "${clientCertConfig}" does not exist.`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
const clientCertConfigFileContent = fs.readFileSync(clientCertConfig, 'utf8');
|
||||
let clientCertConfigJson;
|
||||
|
||||
try {
|
||||
clientCertConfigJson = JSON.parse(clientCertConfigFileContent);
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`Failed to parse Client Certificate Config JSON: ${err.message}`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_INVALID_JSON);
|
||||
}
|
||||
|
||||
if (clientCertConfigJson?.enabled && Array.isArray(clientCertConfigJson?.certs)) {
|
||||
if (brunoConfig.clientCertificates) {
|
||||
brunoConfig.clientCertificates.certs.push(...clientCertConfigJson.certs);
|
||||
} else {
|
||||
brunoConfig.clientCertificates = { certs: clientCertConfigJson.certs };
|
||||
}
|
||||
console.log(chalk.green(`Client certificates has been added`));
|
||||
} else {
|
||||
console.warn(chalk.yellow(`Client certificate configuration is enabled, but it either contains no valid "certs" array or the added configuration has been set to false`));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`Unexpected error: ${err.message}`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (filename && filename.length) {
|
||||
const pathExists = await exists(filename);
|
||||
if (!pathExists) {
|
||||
@ -392,6 +457,9 @@ const handler = async function (argv) {
|
||||
if (insecure) {
|
||||
options['insecure'] = true;
|
||||
}
|
||||
if (disableCookies) {
|
||||
options['disableCookies'] = true;
|
||||
}
|
||||
if (cacert && cacert.length) {
|
||||
if (insecure) {
|
||||
console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`));
|
||||
@ -525,6 +593,35 @@ const handler = async function (argv) {
|
||||
suitename: bruFilepath.replace('.bru', '')
|
||||
});
|
||||
|
||||
if (reporterSkipAllHeaders) {
|
||||
results.forEach((result) => {
|
||||
result.request.headers = {};
|
||||
result.response.headers = {};
|
||||
});
|
||||
}
|
||||
|
||||
const deleteHeaderIfExists = (headers, header) => {
|
||||
if (headers && headers[header]) {
|
||||
delete headers[header];
|
||||
}
|
||||
};
|
||||
|
||||
if (reporterSkipHeaders?.length) {
|
||||
results.forEach((result) => {
|
||||
if (result.request?.headers) {
|
||||
reporterSkipHeaders.forEach((header) => {
|
||||
deleteHeaderIfExists(result.request.headers, header);
|
||||
});
|
||||
}
|
||||
if (result.response?.headers) {
|
||||
reporterSkipHeaders.forEach((header) => {
|
||||
deleteHeaderIfExists(result.response.headers, header);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// bail if option is set and there is a failure
|
||||
if (bail) {
|
||||
const requestFailure = result?.error;
|
||||
|
@ -20,6 +20,7 @@ const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-he
|
||||
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
|
||||
const path = require('path');
|
||||
const { createFormData } = require('../utils/common');
|
||||
const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies');
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
|
||||
const onConsoleLog = (type, args) => {
|
||||
@ -178,6 +179,14 @@ const runSingleRequest = async function (
|
||||
});
|
||||
}
|
||||
|
||||
//set cookies if enabled
|
||||
if (!options.disableCookies) {
|
||||
const cookieString = getCookieStringForUrl(request.url);
|
||||
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
||||
request.headers['cookie'] = cookieString;
|
||||
}
|
||||
}
|
||||
|
||||
// stringify the request url encoded params
|
||||
if (request.headers['content-type'] === 'application/x-www-form-urlencoded') {
|
||||
request.data = qs.stringify(request.data);
|
||||
@ -220,6 +229,11 @@ const runSingleRequest = async function (
|
||||
// Prevents the duration on leaking to the actual result
|
||||
responseTime = response.headers.get('request-duration');
|
||||
response.headers.delete('request-duration');
|
||||
|
||||
//save cookies if enabled
|
||||
if (!options.disableCookies) {
|
||||
saveCookies(request.url, response.headers);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.response) {
|
||||
response = err.response;
|
||||
|
100
packages/bruno-cli/src/utils/cookies.js
Normal file
100
packages/bruno-cli/src/utils/cookies.js
Normal file
@ -0,0 +1,100 @@
|
||||
const { Cookie, CookieJar } = require('tough-cookie');
|
||||
const each = require('lodash/each');
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
const addCookieToJar = (setCookieHeader, requestUrl) => {
|
||||
const cookie = Cookie.parse(setCookieHeader, { loose: true });
|
||||
cookieJar.setCookieSync(cookie, requestUrl, {
|
||||
ignoreError: true // silently ignore things like parse errors and invalid domains
|
||||
});
|
||||
};
|
||||
|
||||
const getCookiesForUrl = (url) => {
|
||||
return cookieJar.getCookiesSync(url);
|
||||
};
|
||||
|
||||
const getCookieStringForUrl = (url) => {
|
||||
const cookies = getCookiesForUrl(url);
|
||||
|
||||
if (!Array.isArray(cookies) || !cookies.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now());
|
||||
|
||||
return validCookies.map((cookie) => cookie.cookieString()).join('; ');
|
||||
};
|
||||
|
||||
const getDomainsWithCookies = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const domainCookieMap = {};
|
||||
|
||||
cookieJar.store.getAllCookies((err, cookies) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
cookies.forEach((cookie) => {
|
||||
if (!domainCookieMap[cookie.domain]) {
|
||||
domainCookieMap[cookie.domain] = [cookie];
|
||||
} else {
|
||||
domainCookieMap[cookie.domain].push(cookie);
|
||||
}
|
||||
});
|
||||
|
||||
const domains = Object.keys(domainCookieMap);
|
||||
const domainsWithCookies = [];
|
||||
|
||||
each(domains, (domain) => {
|
||||
const cookies = domainCookieMap[domain];
|
||||
const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now());
|
||||
|
||||
if (validCookies.length) {
|
||||
domainsWithCookies.push({
|
||||
domain,
|
||||
cookies: validCookies,
|
||||
cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resolve(domainsWithCookies);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteCookiesForDomain = (domain) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
cookieJar.store.removeCookies(domain, null, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const saveCookies = (url, headers) => {
|
||||
let setCookieHeaders = [];
|
||||
if (headers['set-cookie']) {
|
||||
setCookieHeaders = Array.isArray(headers['set-cookie'])
|
||||
? headers['set-cookie']
|
||||
: [headers['set-cookie']];
|
||||
for (let setCookieHeader of setCookieHeaders) {
|
||||
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
|
||||
addCookieToJar(setCookieHeader, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addCookieToJar,
|
||||
getCookiesForUrl,
|
||||
getCookieStringForUrl,
|
||||
getDomainsWithCookies,
|
||||
deleteCookiesForDomain,
|
||||
saveCookies
|
||||
};
|
@ -2,7 +2,7 @@ const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chokidar = require('chokidar');
|
||||
const { hasBruExtension } = require('../utils/filesystem');
|
||||
const { hasBruExtension, isWSLPath, normalizeAndResolvePath, normalizeWslPath } = require('../utils/filesystem');
|
||||
const { bruToEnvJson, bruToJson, collectionBruToJson } = require('../bru');
|
||||
const { dotenvToJson } = require('@usebruno/lang');
|
||||
|
||||
@ -445,11 +445,11 @@ class Watcher {
|
||||
ignoreInitial: false,
|
||||
usePolling: watchPath.startsWith('\\\\') || forcePolling ? true : false,
|
||||
ignored: (filepath) => {
|
||||
const normalizedPath = filepath.replace(/\\/g, '/');
|
||||
const normalizedPath = isWSLPath(filepath) ? normalizeWslPath(filepath) : normalizeAndResolvePath(filepath);
|
||||
const relativePath = path.relative(watchPath, normalizedPath);
|
||||
|
||||
return ignores.some((ignorePattern) => {
|
||||
const normalizedIgnorePattern = ignorePattern.replace(/\\/g, '/');
|
||||
const normalizedIgnorePattern = isWSLPath(ignorePattern) ? normalizeWslPath(ignorePattern) : ignorePattern.replace(/\\/g, '/');
|
||||
return relativePath === normalizedIgnorePattern || relativePath.startsWith(normalizedIgnorePattern);
|
||||
});
|
||||
},
|
||||
|
@ -63,6 +63,10 @@ class GlobalEnvironmentsStore {
|
||||
|
||||
addGlobalEnvironment({ uid, name, variables = [] }) {
|
||||
let globalEnvironments = this.getGlobalEnvironments();
|
||||
const existingEnvironment = globalEnvironments.find(env => env?.name == name);
|
||||
if (existingEnvironment) {
|
||||
throw new Error('Environment with the same name already exists');
|
||||
}
|
||||
globalEnvironments.push({
|
||||
uid,
|
||||
name,
|
||||
|
@ -6,10 +6,34 @@ const { safeStorage } = require('electron');
|
||||
const ELECTRONSAFESTORAGE_ALGO = '00';
|
||||
const AES256_ALGO = '01';
|
||||
|
||||
// AES-256 encryption and decryption functions
|
||||
function deriveKeyAndIv(password, keyLength, ivLength) {
|
||||
const key = Buffer.alloc(keyLength);
|
||||
const iv = Buffer.alloc(ivLength);
|
||||
const derivedBytes = [];
|
||||
let lastHash = null;
|
||||
|
||||
while (Buffer.concat(derivedBytes).length < keyLength + ivLength) {
|
||||
const hash = crypto.createHash('md5');
|
||||
if (lastHash) {
|
||||
hash.update(lastHash);
|
||||
}
|
||||
hash.update(Buffer.from(password, 'utf8'));
|
||||
lastHash = hash.digest();
|
||||
derivedBytes.push(lastHash);
|
||||
}
|
||||
|
||||
const concatenatedBytes = Buffer.concat(derivedBytes);
|
||||
concatenatedBytes.copy(key, 0, 0, keyLength);
|
||||
concatenatedBytes.copy(iv, 0, keyLength, keyLength + ivLength);
|
||||
|
||||
return { key, iv };
|
||||
}
|
||||
|
||||
function aes256Encrypt(data) {
|
||||
const key = machineIdSync();
|
||||
const cipher = crypto.createCipher('aes-256-cbc', key);
|
||||
const rawKey = machineIdSync();
|
||||
const iv = Buffer.alloc(16, 0); // Default IV for new encryption
|
||||
const key = crypto.createHash('sha256').update(rawKey).digest(); // Derive a 32-byte key
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
||||
let encrypted = cipher.update(data, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
|
||||
@ -17,14 +41,28 @@ function aes256Encrypt(data) {
|
||||
}
|
||||
|
||||
function aes256Decrypt(data) {
|
||||
const key = machineIdSync();
|
||||
const decipher = crypto.createDecipher('aes-256-cbc', key);
|
||||
let decrypted = decipher.update(data, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
const rawKey = machineIdSync();
|
||||
|
||||
return decrypted;
|
||||
// Attempt to decrypt using new method first
|
||||
const iv = Buffer.alloc(16, 0); // Default IV for new encryption
|
||||
const key = crypto.createHash('sha256').update(rawKey).digest(); // Derive a 32-byte key
|
||||
|
||||
try {
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
||||
let decrypted = decipher.update(data, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
return decrypted;
|
||||
} catch (err) {
|
||||
// If decryption fails, fall back to old key derivation
|
||||
const { key: oldKey, iv: oldIv } = deriveKeyAndIv(rawKey, 32, 16);
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', oldKey, oldIv);
|
||||
let decrypted = decipher.update(data, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
return decrypted;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// electron safe storage encryption and decryption functions
|
||||
function safeStorageEncrypt(str) {
|
||||
let encryptedStringBuffer = safeStorage.encryptString(str);
|
||||
|
@ -22,6 +22,13 @@ describe('Encryption and Decryption Tests', () => {
|
||||
expect(() => decryptString('garbage')).toThrow('Decrypt failed: unrecognized string format');
|
||||
});
|
||||
|
||||
it.skip('string encrypted using createCipher (< node 20) should be decrypted properly', () => {
|
||||
const encryptedString = '$01:2738e0e6a38bcde5fd80141ceadc9b67bc7b1fca7e398c552c1ca2bace28eb57';
|
||||
const decryptedValue = decryptString(encryptedString);
|
||||
|
||||
expect(decryptedValue).toBe('bruno is awesome');
|
||||
});
|
||||
|
||||
it('decrypt should throw an error for invalid algorithm', () => {
|
||||
const invalidAlgo = '$99:abcdefg';
|
||||
|
||||
|
@ -65,6 +65,10 @@ class Bru {
|
||||
this.envVariables[key] = value;
|
||||
}
|
||||
|
||||
deleteEnvVar(key) {
|
||||
delete this.envVariables[key];
|
||||
}
|
||||
|
||||
getGlobalEnvVar(key) {
|
||||
return this._interpolate(this.globalEnvironmentVariables[key]);
|
||||
}
|
||||
|
@ -39,6 +39,12 @@ const addBruShimToContext = (vm, bru) => {
|
||||
vm.setProp(bruObject, 'setEnvVar', setEnvVar);
|
||||
setEnvVar.dispose();
|
||||
|
||||
let deleteEnvVar = vm.newFunction('deleteEnvVar', function (key) {
|
||||
return marshallToVm(bru.deleteEnvVar(vm.dump(key)), vm);
|
||||
});
|
||||
vm.setProp(bruObject, 'deleteEnvVar', deleteEnvVar);
|
||||
deleteEnvVar.dispose();
|
||||
|
||||
let getGlobalEnvVar = vm.newFunction('getGlobalEnvVar', function (key) {
|
||||
return marshallToVm(bru.getGlobalEnvVar(vm.dump(key)), vm);
|
||||
});
|
||||
|
@ -5,7 +5,7 @@ meta {
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://gloutnikov.com/post/2024/bruno.png
|
||||
url: https://www.usebruno.com/favicon.ico
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@ -13,7 +13,7 @@ get {
|
||||
tests {
|
||||
test("should return parsed xml", function() {
|
||||
const headers = res.getHeaders();
|
||||
expect(headers['content-type']).to.eql("image/png");
|
||||
expect(headers['content-type']).to.eql("image/x-icon");
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ assert {
|
||||
script:pre-request {
|
||||
bru.setVar("rUser", {
|
||||
full_name: 'Bruno',
|
||||
age: 4,
|
||||
age: 5,
|
||||
'fav-food': ['egg', 'meat'],
|
||||
'want.attention': true
|
||||
});
|
||||
@ -49,7 +49,7 @@ script:pre-request {
|
||||
tests {
|
||||
test("should return json", function() {
|
||||
const expectedResponse = `Hi, I am Bruno,
|
||||
I am 4 years old.
|
||||
I am 5 years old.
|
||||
My favorite food is egg and meat.
|
||||
I like attention: true`;
|
||||
expect(res.getBody()).to.equal(expectedResponse);
|
||||
|
@ -44,12 +44,12 @@ Bruno is offline-only. There are no plans to add cloud-sync to Bruno, ever. We v
|
||||
|
||||
![bruno](assets/images/landing-2.png) <br /><br />
|
||||
|
||||
## Golden Edition ✨
|
||||
## Commercial Versions ✨
|
||||
|
||||
Majority of our features are free and open source.
|
||||
We strive to strike a harmonious balance between [open-source principles and sustainability](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
You can buy the [Golden Edition](https://www.usebruno.com/pricing) for a one-time payment of **$19**! <br/>
|
||||
You can explore our [paid versions](https://www.usebruno.com/pricing) to see if there are additional features that you or your team may find useful! <br/>
|
||||
|
||||
## Table of Contents
|
||||
- [Installation](#installation)
|
||||
|
Loading…
Reference in New Issue
Block a user