mirror of
https://github.com/usebruno/bruno.git
synced 2025-01-23 14:18:41 +01:00
Merge branch 'main' into fix/remove-jsonbigint-from-cli
This commit is contained in:
commit
4dcaaab52c
10
.github/workflows/npm-bru-cli.yml
vendored
10
.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
|
||||
@ -20,7 +15,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
|
||||
|
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
|
||||
|
6580
package-lock.json
generated
6580
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
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)
|
||||
|
@ -10,6 +10,12 @@ import Modal from 'components/Modal';
|
||||
const CreateEnvironment = ({ collection, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
|
||||
// todo: Add this to global env too.
|
||||
const validateEnvironmentName = (name) => {
|
||||
return !collection?.environments?.some((env) => env?.name?.toLowerCase().trim() === name?.toLowerCase().trim());
|
||||
};
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
@ -17,9 +23,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))
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
|
@ -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': {
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 = () => {
|
||||
@ -60,7 +60,7 @@ const trackStart = () => {
|
||||
event: 'start',
|
||||
properties: {
|
||||
os: platformLib.os.family,
|
||||
version: '1.34.2'
|
||||
version: '1.36.0'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -9,6 +9,7 @@ const getReadNotificationIds = () => {
|
||||
return readNotificationIds;
|
||||
} catch (err) {
|
||||
toast.error('An error occurred while fetching read notifications');
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
@ -58,14 +59,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;
|
||||
},
|
||||
|
@ -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');
|
||||
|
@ -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
|
||||
@ -815,6 +816,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 = {};
|
||||
@ -850,6 +868,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,
|
||||
@ -861,6 +886,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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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', () => {
|
||||
|
@ -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,15 @@ 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() };
|
||||
}
|
||||
} else if (contentType && contentType.includes('application/xml')) {
|
||||
return { data: request.data };
|
||||
}
|
||||
|
||||
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,14 +49,18 @@ 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')) {
|
||||
} else if (contentType.includes('xml')) {
|
||||
body.mode = 'xml';
|
||||
body.xml = parsedBody;
|
||||
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||
|
@ -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) => {
|
||||
|
@ -40,6 +40,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"
|
||||
|
@ -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 = [];
|
||||
@ -211,6 +271,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 +324,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 +377,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 +387,7 @@ const handler = async function (argv) {
|
||||
filename,
|
||||
cacert,
|
||||
ignoreTruststore,
|
||||
disableCookies,
|
||||
env,
|
||||
envVar,
|
||||
insecure,
|
||||
@ -312,7 +399,10 @@ const handler = async function (argv) {
|
||||
reporterHtml,
|
||||
sandbox,
|
||||
testsOnly,
|
||||
bail
|
||||
bail,
|
||||
reporterSkipAllHeaders,
|
||||
reporterSkipHeaders,
|
||||
clientCertConfig
|
||||
} = argv;
|
||||
const collectionPath = process.cwd();
|
||||
|
||||
@ -329,6 +419,47 @@ 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 {
|
||||
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);
|
||||
@ -392,6 +523,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`));
|
||||
@ -516,7 +650,8 @@ const handler = async function (argv) {
|
||||
processEnvVars,
|
||||
brunoConfig,
|
||||
collectionRoot,
|
||||
runtime
|
||||
runtime,
|
||||
collection
|
||||
);
|
||||
|
||||
results.push({
|
||||
@ -525,6 +660,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;
|
||||
|
@ -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: {
|
||||
|
@ -1,23 +1,223 @@
|
||||
const { get, each, filter } = require('lodash');
|
||||
const { get, each, filter, find, compact } = require('lodash');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const decomment = require('decomment');
|
||||
const crypto = require('node:crypto');
|
||||
|
||||
const prepareRequest = (request, collectionRoot) => {
|
||||
const headers = {};
|
||||
let contentTypeDefined = false;
|
||||
const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let headers = new Map();
|
||||
|
||||
// collection headers
|
||||
each(get(collectionRoot, 'request.headers', []), (h) => {
|
||||
if (h.enabled) {
|
||||
headers[h.name] = h.value;
|
||||
if (h.name.toLowerCase() === 'content-type') {
|
||||
contentTypeDefined = true;
|
||||
}
|
||||
let collectionHeaders = get(collection, 'root.request.headers', []);
|
||||
collectionHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header.value);
|
||||
}
|
||||
});
|
||||
|
||||
each(request.headers, (h) => {
|
||||
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;
|
||||
};
|
||||
|
||||
const prepareRequest = (item = {}, collection = {}) => {
|
||||
const request = item?.request;
|
||||
const brunoConfig = get(collection, 'brunoConfig', {});
|
||||
const headers = {};
|
||||
let contentTypeDefined = false;
|
||||
|
||||
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(get(request, 'headers', []), (h) => {
|
||||
if (h.enabled) {
|
||||
headers[h.name] = h.value;
|
||||
if (h.name.toLowerCase() === 'content-type') {
|
||||
@ -34,7 +234,7 @@ const prepareRequest = (request, collectionRoot) => {
|
||||
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 = {
|
||||
@ -151,10 +351,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;
|
||||
};
|
||||
|
||||
|
@ -20,6 +20,7 @@ const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-he
|
||||
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
|
||||
const path = require('path');
|
||||
const { createFormData, parseDataFromResponse } = require('../utils/common');
|
||||
const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies');
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
|
||||
const onConsoleLog = (type, args) => {
|
||||
@ -35,13 +36,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';
|
||||
|
||||
@ -49,10 +54,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(
|
||||
@ -178,6 +180,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);
|
||||
@ -223,6 +233,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) {
|
||||
const { data } = parseDataFromResponse(err?.response);
|
||||
@ -251,7 +266,7 @@ const runSingleRequest = async function (
|
||||
data: null,
|
||||
responseTime: 0
|
||||
},
|
||||
error: err.message,
|
||||
error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!',
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
nextRequestName: nextRequestName
|
||||
@ -282,10 +297,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(
|
||||
@ -330,7 +342,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(
|
||||
|
@ -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', '')
|
||||
}
|
||||
};
|
||||
|
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
|
||||
};
|
@ -9,7 +9,7 @@ describe('prepare-request: prepareRequest', () => {
|
||||
const expected = `{
|
||||
\"test\": \"{{someVar}}\"
|
||||
}`;
|
||||
const result = prepareRequest({ body });
|
||||
const result = prepareRequest({ request: { body } });
|
||||
expect(result.data).toEqual(expected);
|
||||
});
|
||||
|
||||
@ -18,7 +18,7 @@ describe('prepare-request: prepareRequest', () => {
|
||||
const expected = `{
|
||||
\"test\": {{someVar}}
|
||||
}`;
|
||||
const result = prepareRequest({ body });
|
||||
const result = prepareRequest({ request: { body } });
|
||||
expect(result.data).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
});
|
||||
},
|
||||
|
@ -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,9 @@ const {
|
||||
isWSLPath,
|
||||
normalizeWslPath,
|
||||
normalizeAndResolvePath,
|
||||
safeToRename
|
||||
safeToRename,
|
||||
isWindowsOS,
|
||||
isValidFilename
|
||||
} = require('../utils/filesystem');
|
||||
const { openCollectionDialog } = require('../app/collections');
|
||||
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
|
||||
@ -201,7 +205,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) {
|
||||
@ -358,23 +364,35 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
const newBruFilePath = bruFile.replace(oldPath, newPath);
|
||||
moveRequestUid(bruFile, newBruFilePath);
|
||||
}
|
||||
return fs.renameSync(oldPath, newPath);
|
||||
|
||||
if (isWindowsOS() && !isWSLPath(oldPath)) {
|
||||
const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`);
|
||||
|
||||
await fsExtra.copy(oldPath, tempDir);
|
||||
await fsExtra.move(tempDir, newPath, { overwrite: true });
|
||||
await fsExtra.remove(oldPath);
|
||||
} else {
|
||||
await fs.renameSync(oldPath, newPath);
|
||||
}
|
||||
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;
|
||||
|
@ -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);
|
||||
|
@ -160,6 +160,24 @@ const sanitizeDirectoryName = (name) => {
|
||||
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-');
|
||||
};
|
||||
|
||||
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) => {
|
||||
try {
|
||||
// If the new path doesn't exist, it's safe to rename
|
||||
@ -170,7 +188,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 +222,7 @@ module.exports = {
|
||||
searchForFiles,
|
||||
searchForBruFiles,
|
||||
sanitizeDirectoryName,
|
||||
safeToRename
|
||||
isWindowsOS,
|
||||
safeToRename,
|
||||
isValidFilename
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -24,7 +24,7 @@ get(data, '..items[?]', { id: 2, amount: 20 })
|
||||
```
|
||||
Array mapping [?] with corresponding mapper function
|
||||
```js
|
||||
get(data, '..items[?].amount', i => i.amount + 10)
|
||||
get(data, '..items..amount[?]', amt => amt + 10)
|
||||
```
|
||||
|
||||
### Publish to Npm Registry
|
||||
|
@ -1,6 +1,7 @@
|
||||
headers {
|
||||
check: again
|
||||
token: {{collection_pre_var_token}}
|
||||
collection-header: collection-header-value
|
||||
}
|
||||
|
||||
auth {
|
||||
@ -14,6 +15,28 @@ auth:bearer {
|
||||
vars:pre-request {
|
||||
collection_pre_var: collection_pre_var_value
|
||||
collection_pre_var_token: {{request_pre_var_token}}
|
||||
collection-var: collection-var-value
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
// used by `scripting/js/folder-collection script-tests`
|
||||
const shouldTestCollectionScripts = bru.getVar('should-test-collection-scripts');
|
||||
if(shouldTestCollectionScripts) {
|
||||
bru.setVar('collection-var-set-by-collection-script', 'collection-var-value-set-by-collection-script');
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
// used by `scripting/js/folder-collection script-tests`
|
||||
const shouldTestCollectionScripts = bru.getVar('should-test-collection-scripts');
|
||||
const collectionVar = bru.getVar("collection-var-set-by-collection-script");
|
||||
if (shouldTestCollectionScripts && collectionVar) {
|
||||
test("collection level test - should get the var that was set by the collection script", function() {
|
||||
expect(collectionVar).to.equal("collection-var-value-set-by-collection-script");
|
||||
});
|
||||
bru.setVar('collection-var-set-by-collection-script', null);
|
||||
bru.setVar('should-test-collection-scripts', null);
|
||||
}
|
||||
}
|
||||
|
||||
docs {
|
||||
|
@ -5,7 +5,7 @@ meta {
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://www.usebruno.com/images/landing-2.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");
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
meta {
|
||||
name: bru
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
folder-var: folder-var-value
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
meta {
|
||||
name: getCollectionVar
|
||||
type: http
|
||||
seq: 9
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
tests {
|
||||
test("should get collection var in scripts", function() {
|
||||
const testVar = bru.getCollectionVar("collection-var");
|
||||
expect(testVar).to.equal("collection-var-value");
|
||||
});
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
meta {
|
||||
name: getFolderVar
|
||||
type: http
|
||||
seq: 8
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
tests {
|
||||
test("should get folder var in scripts", function() {
|
||||
const testVar = bru.getFolderVar("folder-var");
|
||||
expect(testVar).to.equal("folder-var-value");
|
||||
});
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: getRequestVar
|
||||
type: http
|
||||
seq: 7
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
request-var: request-var-value
|
||||
}
|
||||
|
||||
tests {
|
||||
test("should get request var in scripts", function() {
|
||||
const testVar = bru.getRequestVar("request-var");
|
||||
expect(testVar).to.equal("request-var-value");
|
||||
});
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
meta {
|
||||
name: folder-collection script-tests pre
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.setVar('should-test-collection-scripts', true);
|
||||
bru.setVar('should-test-folder-scripts', true);
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
meta {
|
||||
name: folder-collection script-tests
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
// do not delete - the collection/folder scripts/tests run during this request execution
|
||||
}
|
||||
|
||||
tests {
|
||||
const collectionHeader = req.getHeader("collection-header");
|
||||
const folderHeader = req.getHeader("folder-header");
|
||||
|
||||
test("should get the header value set at collection level", function() {
|
||||
expect(collectionHeader).to.equal("collection-header-value");
|
||||
});
|
||||
|
||||
test("should get the header value set at folder level", function() {
|
||||
expect(folderHeader).to.equal("folder-header-value");
|
||||
});
|
||||
}
|
28
packages/bruno-tests/collection/scripting/js/folder.bru
Normal file
28
packages/bruno-tests/collection/scripting/js/folder.bru
Normal file
@ -0,0 +1,28 @@
|
||||
meta {
|
||||
name: js
|
||||
}
|
||||
|
||||
headers {
|
||||
folder-header: folder-header-value
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
// used by `scripting/js/folder-collection script-tests`
|
||||
const shouldTestFolderScripts = bru.getVar('should-test-folder-scripts');
|
||||
if(shouldTestFolderScripts) {
|
||||
bru.setVar('folder-var-set-by-folder-script', 'folder-var-value-set-by-folder-script');
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
// used by `scripting/js/folder-collection script-tests`
|
||||
const shouldTestFolderScripts = bru.getVar('should-test-folder-scripts');
|
||||
const folderVar = bru.getVar("folder-var-set-by-folder-script");
|
||||
if (shouldTestFolderScripts && folderVar) {
|
||||
test("folder level test - should get the var that was set by the folder script", function() {
|
||||
expect(folderVar).to.equal("folder-var-value-set-by-folder-script");
|
||||
});
|
||||
bru.setVar('folder-var-set-by-folder-script', null);
|
||||
bru.setVar('should-test-folder-scripts', null);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
12
packages/bruno-tests/sandwich_exec/bruno.json
Normal file
12
packages/bruno-tests/sandwich_exec/bruno.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "sandwich_exec",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
],
|
||||
"scripts": {
|
||||
"flow": "sandwich"
|
||||
}
|
||||
}
|
13
packages/bruno-tests/sandwich_exec/collection.bru
Normal file
13
packages/bruno-tests/sandwich_exec/collection.bru
Normal file
@ -0,0 +1,13 @@
|
||||
script:pre-request {
|
||||
console.log("collection pre");
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
{
|
||||
console.log("collection post");
|
||||
const sequence = bru.getVar('sequence') || [];
|
||||
sequence.push(1);
|
||||
bru.setVar('sequence', sequence);
|
||||
console.log("sequence", bru.getVar('sequence'));
|
||||
}
|
||||
}
|
16
packages/bruno-tests/sandwich_exec/folder/folder.bru
Normal file
16
packages/bruno-tests/sandwich_exec/folder/folder.bru
Normal file
@ -0,0 +1,16 @@
|
||||
meta {
|
||||
name: folder
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
console.log("folder pre");
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
{
|
||||
const sequence = bru.getVar('sequence') || [];
|
||||
sequence.push(2);
|
||||
bru.setVar('sequence', sequence);
|
||||
}
|
||||
console.log("folder post");
|
||||
}
|
33
packages/bruno-tests/sandwich_exec/folder/request.bru
Normal file
33
packages/bruno-tests/sandwich_exec/folder/request.bru
Normal file
@ -0,0 +1,33 @@
|
||||
meta {
|
||||
name: request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://www.example.com
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
console.log("request pre");
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
{
|
||||
console.log("request post");
|
||||
|
||||
const sequence = bru.getVar('sequence') || [];
|
||||
sequence.push(3);
|
||||
bru.setVar('sequence', sequence);
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
test("sandwich script execution is proper", function() {
|
||||
const sequence = bru.getVar('sequence');
|
||||
bru.setVar('sequence', null);
|
||||
expect(sequence.toString()).to.equal([3,2,1].toString());
|
||||
});
|
||||
}
|
12
packages/bruno-tests/sequential_exec/bruno.json
Normal file
12
packages/bruno-tests/sequential_exec/bruno.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "sequential_exec",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
],
|
||||
"scripts": {
|
||||
"flow": "sequential"
|
||||
}
|
||||
}
|
12
packages/bruno-tests/sequential_exec/collection.bru
Normal file
12
packages/bruno-tests/sequential_exec/collection.bru
Normal file
@ -0,0 +1,12 @@
|
||||
script:pre-request {
|
||||
console.log("collection pre");
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
{
|
||||
console.log("collection post");
|
||||
const sequence = bru.getVar('sequence') || [];
|
||||
sequence.push(1);
|
||||
bru.setVar('sequence', sequence);
|
||||
}
|
||||
}
|
16
packages/bruno-tests/sequential_exec/folder/folder.bru
Normal file
16
packages/bruno-tests/sequential_exec/folder/folder.bru
Normal file
@ -0,0 +1,16 @@
|
||||
meta {
|
||||
name: folder
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
console.log("folder pre");
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
{
|
||||
console.log("folder post");
|
||||
const sequence = bru.getVar('sequence') || [];
|
||||
sequence.push(2);
|
||||
bru.setVar('sequence', sequence);
|
||||
}
|
||||
}
|
34
packages/bruno-tests/sequential_exec/folder/request.bru
Normal file
34
packages/bruno-tests/sequential_exec/folder/request.bru
Normal file
@ -0,0 +1,34 @@
|
||||
meta {
|
||||
name: request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://www.example.com
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
console.log("request pre");
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
{
|
||||
console.log("request post");
|
||||
const sequence = bru.getVar('sequence') || [];
|
||||
sequence.push(3);
|
||||
bru.setVar('sequence', sequence);
|
||||
|
||||
console.log("sequence", bru.getVar('sequence'));
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
test("sequential script execution is proper", function() {
|
||||
const sequence = bru.getVar('sequence');
|
||||
bru.setVar('sequence', null);
|
||||
expect(sequence.toString()).to.equal([1,2,3].toString());
|
||||
});
|
||||
}
|
@ -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