From b82a2c33121ff773d3cb02b1bdd2561e24fd3de0 Mon Sep 17 00:00:00 2001 From: Matthew Dickinson Date: Fri, 18 Oct 2024 17:46:51 -0400 Subject: [PATCH 01/33] Added cookie support to CLI requests --- packages/bruno-cli/package.json | 1 + packages/bruno-cli/src/commands/run.js | 9 ++ .../src/runner/run-single-request.js | 14 +++ packages/bruno-cli/src/utils/cookies.js | 100 ++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 packages/bruno-cli/src/utils/cookies.js diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json index 5020f8475a..f62f9dc647 100644 --- a/packages/bruno-cli/package.json +++ b/packages/bruno-cli/package.json @@ -41,6 +41,7 @@ "lodash": "^4.17.21", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", + "tough-cookie": "^4.1.3", "@usebruno/vm2": "^3.9.13", "xmlbuilder": "^15.1.1", "yargs": "^17.6.2" diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 58b3cdf803..6f5b942d49 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -211,6 +211,11 @@ const builder = async (yargs) => { description: 'The specified custom CA certificate (--cacert) will be used exclusively and the default truststore is ignored, if this option is specified. Evaluated in combination with "--cacert" only.' }) + .option('use-cookies', { + type: 'boolean', + default: false, + description: 'Automatically save and sent cookies with requests' + }) .option('env', { describe: 'Environment variables', type: 'string' @@ -301,6 +306,7 @@ const handler = async function (argv) { filename, cacert, ignoreTruststore, + useCookies, env, envVar, insecure, @@ -392,6 +398,9 @@ const handler = async function (argv) { if (insecure) { options['insecure'] = true; } + if (useCookies) { + options['useCookies'] = true; + } if (cacert && cacert.length) { if (insecure) { console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`)); diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index cb59c78ba5..9a4f223705 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -20,6 +20,7 @@ const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-he const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util'); const path = require('path'); const { createFormData } = require('../utils/common'); +const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; const onConsoleLog = (type, args) => { @@ -178,6 +179,14 @@ const runSingleRequest = async function ( }); } + //set cookies if enabled + if (options.useCookies) { + const cookieString = getCookieStringForUrl(request.url); + if (cookieString && typeof cookieString === 'string' && cookieString.length) { + request.headers['cookie'] = cookieString; + } + } + // stringify the request url encoded params if (request.headers['content-type'] === 'application/x-www-form-urlencoded') { request.data = qs.stringify(request.data); @@ -220,6 +229,11 @@ const runSingleRequest = async function ( // Prevents the duration on leaking to the actual result responseTime = response.headers.get('request-duration'); response.headers.delete('request-duration'); + + //save cookies if enabled + if (options.useCookies) { + saveCookies(request.url, response.headers); + } } catch (err) { if (err?.response) { response = err.response; diff --git a/packages/bruno-cli/src/utils/cookies.js b/packages/bruno-cli/src/utils/cookies.js new file mode 100644 index 0000000000..acb58b5052 --- /dev/null +++ b/packages/bruno-cli/src/utils/cookies.js @@ -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 +}; From 4894ac2754bd2f56e98ef92d9523065df32b8df4 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Fri, 15 Nov 2024 19:26:30 +0530 Subject: [PATCH 02/33] fix: fixed failing test --- packages/bruno-tests/collection/preview/image/bruno.bru | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-tests/collection/preview/image/bruno.bru b/packages/bruno-tests/collection/preview/image/bruno.bru index bb773d91c1..2818d70a8f 100644 --- a/packages/bruno-tests/collection/preview/image/bruno.bru +++ b/packages/bruno-tests/collection/preview/image/bruno.bru @@ -5,7 +5,7 @@ meta { } get { - url: https://www.usebruno.com/images/landing-2.png + url: https://gloutnikov.com/post/2024/bruno.png body: none auth: none } From 9a325caeeeb06d6428691855791ab7a638b82d70 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 19 Nov 2024 10:22:55 +0530 Subject: [PATCH 03/33] feat: add ssl-cert-list option for secure connections in CLI run command --- packages/bruno-cli/src/commands/run.js | 43 ++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 58b3cdf803..e156474a92 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -259,6 +259,11 @@ const builder = async (yargs) => { type: 'boolean', description: 'Stop execution after a failure of a request, test, or assertion' }) + .option('ssl-cert-list', { + type: 'string', + description: 'Path to the SSL client certificate list 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') @@ -292,7 +297,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 --ssl-cert-list ssl-cert-list.json', 'Run a request with SSL client certificate list'); }; const handler = async function (argv) { @@ -312,7 +318,8 @@ const handler = async function (argv) { reporterHtml, sandbox, testsOnly, - bail + bail, + sslCertList } = argv; const collectionPath = process.cwd(); @@ -330,6 +337,38 @@ const handler = async function (argv) { const brunoConfig = JSON.parse(brunoConfigFile); const collectionRoot = getCollectionRoot(collectionPath); + if (sslCertList) { + try { + const sslCertListPathExists = await exists(sslCertList); + if (!sslCertListPathExists) { + console.error(chalk.red(`SSL Certificate List file "${sslCertList}" does not exist.`)); + process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND); + } + + const sslCertListFile = fs.readFileSync(sslCertList, 'utf8'); + let sslCertListJson; + + try { + sslCertListJson = JSON.parse(sslCertListFile); + } catch (err) { + console.error(chalk.red(`Failed to parse SSL Certificate List JSON: ${err.message}`)); + process.exit(constants.EXIT_STATUS.ERROR_INVALID_JSON); + } + if (brunoConfig.clientCertificates) { + brunoConfig.clientCertificates = { + ...brunoConfig.clientCertificates, + certs: [...brunoConfig.clientCertificates.certs, ...sslCertListJson] + }; + } else { + brunoConfig.clientCertificates = { certs: sslCertListJson }; + } + } catch (err) { + console.error(chalk.red(`Unexpected error: ${err.message}`)); + process.exit(constants.EXIT_STATUS.ERROR_UNKNOWN); + } + } + + if (filename && filename.length) { const pathExists = await exists(filename); if (!pathExists) { From c322baa9c8fe49c5435bf65004a098522e4b3816 Mon Sep 17 00:00:00 2001 From: lohit Date: Wed, 20 Nov 2024 03:22:39 +0530 Subject: [PATCH 04/33] fix: server_rendered logic for newer versions of nextjs (#3509) * fix: server_rendered condition update --- packages/bruno-app/src/components/CodeEditor/index.js | 2 +- packages/bruno-app/src/components/MultiLineEditor/index.js | 2 +- .../bruno-app/src/components/RequestPane/QueryEditor/index.js | 2 +- packages/bruno-app/src/components/SingleLineEditor/index.js | 2 +- packages/bruno-app/src/pages/Bruno/index.js | 2 +- packages/bruno-app/src/pages/_app.js | 3 +-- packages/bruno-app/src/utils/common/codemirror.js | 2 +- 7 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index 8ec68bdf89..f0f1ed55ea 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -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) { diff --git a/packages/bruno-app/src/components/MultiLineEditor/index.js b/packages/bruno-app/src/components/MultiLineEditor/index.js index d548898fef..97b3eca07a 100644 --- a/packages/bruno-app/src/components/MultiLineEditor/index.js +++ b/packages/bruno-app/src/components/MultiLineEditor/index.js @@ -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'); diff --git a/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js b/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js index 3d93aae8de..60bafc8fed 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js @@ -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'); diff --git a/packages/bruno-app/src/components/SingleLineEditor/index.js b/packages/bruno-app/src/components/SingleLineEditor/index.js index 31d0875fd0..16413bdf38 100644 --- a/packages/bruno-app/src/components/SingleLineEditor/index.js +++ b/packages/bruno-app/src/components/SingleLineEditor/index.js @@ -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'); diff --git a/packages/bruno-app/src/pages/Bruno/index.js b/packages/bruno-app/src/pages/Bruno/index.js index 71e24dcfa3..b789e6dddc 100644 --- a/packages/bruno-app/src/pages/Bruno/index.js +++ b/packages/bruno-app/src/pages/Bruno/index.js @@ -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'); diff --git a/packages/bruno-app/src/pages/_app.js b/packages/bruno-app/src/pages/_app.js index 3316380701..08ebe6b0b7 100644 --- a/packages/bruno-app/src/pages/_app.js +++ b/packages/bruno-app/src/pages/_app.js @@ -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; diff --git a/packages/bruno-app/src/utils/common/codemirror.js b/packages/bruno-app/src/utils/common/codemirror.js index cbb1a2b3a3..53cbcf9a09 100644 --- a/packages/bruno-app/src/utils/common/codemirror.js +++ b/packages/bruno-app/src/utils/common/codemirror.js @@ -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'); From dc77ee7c04269a81d40c55137dc894aa2b52efe6 Mon Sep 17 00:00:00 2001 From: lohit Date: Wed, 20 Nov 2024 03:24:29 +0530 Subject: [PATCH 05/33] feat: moved posthog key to process env (#3490) --- packages/bruno-app/.gitignore | 2 ++ .../bruno-app/src/components/Sidebar/GoldenEdition/index.js | 2 +- packages/bruno-app/src/providers/App/useTelemetry.js | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/bruno-app/.gitignore b/packages/bruno-app/.gitignore index 1afc1f3a65..26d2023a39 100644 --- a/packages/bruno-app/.gitignore +++ b/packages/bruno-app/.gitignore @@ -32,3 +32,5 @@ yarn-error.log* # next.js .next/ out/ + +.env \ No newline at end of file diff --git a/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js b/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js index 4335bc2359..ac6acee68e 100644 --- a/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js +++ b/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js @@ -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; diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js index 2d79e0cab8..783560bee9 100644 --- a/packages/bruno-app/src/providers/App/useTelemetry.js +++ b/packages/bruno-app/src/providers/App/useTelemetry.js @@ -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 = () => { From cd629451e4a3a92a0e9d95f5777eba0224b1df7b Mon Sep 17 00:00:00 2001 From: lohit Date: Wed, 20 Nov 2024 03:25:32 +0530 Subject: [PATCH 06/33] fix: checkov CKV2_GHA_1 warning (#3488) * fix: posthog api key as a process env var * fix: checkov tests workflow warning * revert: updates --- .github/workflows/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 30fe01bb4e..aec3d68a0e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: unit-test: name: Unit Tests From ad5b6256555b460b14f9998a4be42a9c981cf22d Mon Sep 17 00:00:00 2001 From: lohit Date: Wed, 20 Nov 2024 03:26:22 +0530 Subject: [PATCH 07/33] fix: checkov CKV_GHA_7 warning (#3489) * fix: posthog api key as a process env var * fix: checkov bru cli workflow warning --------- Co-authored-by: Anoop M D --- .github/workflows/npm-bru-cli.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/npm-bru-cli.yml b/.github/workflows/npm-bru-cli.yml index 62511558ca..ad964a1818 100644 --- a/.github/workflows/npm-bru-cli.yml +++ b/.github/workflows/npm-bru-cli.yml @@ -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 From 41040bc29622d60b691b8bef05051561ac39fdcf Mon Sep 17 00:00:00 2001 From: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com> Date: Wed, 20 Nov 2024 03:29:27 +0530 Subject: [PATCH 08/33] fix: update image URL and adjust tests for age variable in bruno.bru files (#3483) Co-authored-by: Anoop M D --- packages/bruno-tests/collection/preview/image/bruno.bru | 4 ++-- .../collection/string interpolation/runtime vars.bru | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bruno-tests/collection/preview/image/bruno.bru b/packages/bruno-tests/collection/preview/image/bruno.bru index 2818d70a8f..d8c6b74d14 100644 --- a/packages/bruno-tests/collection/preview/image/bruno.bru +++ b/packages/bruno-tests/collection/preview/image/bruno.bru @@ -5,7 +5,7 @@ meta { } get { - url: https://gloutnikov.com/post/2024/bruno.png + url: https://www.usebruno.com/favicon.ico body: none auth: none } @@ -13,7 +13,7 @@ get { tests { test("should return parsed xml", function() { const headers = res.getHeaders(); - expect(headers['content-type']).to.eql("image/png"); + expect(headers['content-type']).to.eql("image/x-icon"); }); } diff --git a/packages/bruno-tests/collection/string interpolation/runtime vars.bru b/packages/bruno-tests/collection/string interpolation/runtime vars.bru index 6cda713e88..6e70647e80 100644 --- a/packages/bruno-tests/collection/string interpolation/runtime vars.bru +++ b/packages/bruno-tests/collection/string interpolation/runtime vars.bru @@ -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); From 7fd7eafdcbf39e40ad6ced12d813273ed333b40f Mon Sep 17 00:00:00 2001 From: lohit Date: Wed, 20 Nov 2024 03:30:50 +0530 Subject: [PATCH 09/33] fix: incorrect call of dispatch fn (#3452) --- .../ReduxStore/slices/global-environments.js | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js b/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js index 6364390f52..7cc19a5a3e 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js @@ -108,9 +108,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 +125,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 +147,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 +159,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 +169,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); }); @@ -228,9 +218,7 @@ export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) => environmentUid, variables })) - .then( - dispatch(_saveGlobalEnvironment({ environmentUid, variables })) - ) + .then(() => dispatch(_saveGlobalEnvironment({ environmentUid, variables }))) .then(resolve) .catch((error) => { reject(error); From c4492b5d9424ab6c22c7fac732e06f04c6032704 Mon Sep 17 00:00:00 2001 From: lohit Date: Wed, 20 Nov 2024 03:31:44 +0530 Subject: [PATCH 10/33] fix: exclude Meta, Alt, Home and End key press for autocomplete trigger (#3441) --- packages/bruno-app/src/components/CodeEditor/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index f0f1ed55ea..cd91c1b467 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -281,9 +281,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)) && /(? Date: Wed, 20 Nov 2024 03:35:55 +0530 Subject: [PATCH 11/33] fix: codemirror styling updates (#3439) --- .../components/CodeEditor/StyledWrapper.js | 14 +++ .../RequestPane/QueryEditor/StyledWrapper.js | 8 ++ packages/bruno-app/src/globalStyles.js | 2 +- packages/bruno-app/src/styles/globals.css | 85 ++++++++++++++++--- 4 files changed, 95 insertions(+), 14 deletions(-) diff --git a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js index 7eba2bc1ff..edcee4cd9f 100644 --- a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js index 6ea15877d3..57b8d49873 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/globalStyles.js b/packages/bruno-app/src/globalStyles.js index 30f264ab1f..c63f23c17f 100644 --- a/packages/bruno-app/src/globalStyles.js +++ b/packages/bruno-app/src/globalStyles.js @@ -233,7 +233,7 @@ const GlobalStyle = createGlobalStyle` } .CodeMirror-hint-active { - background: #89f !important; + background: #08f !important; color: #fff !important; } `; diff --git a/packages/bruno-app/src/styles/globals.css b/packages/bruno-app/src/styles/globals.css index cf132dcd95..c50d118240 100644 --- a/packages/bruno-app/src/styles/globals.css +++ b/packages/bruno-app/src/styles/globals.css @@ -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,24 +44,77 @@ --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; + --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: 2px; - --px-4: 4px; - --px-6: 6px; + --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; + --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: 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,11 @@ --session-header-height: 51px } +.CodeMirror-dialog { + --px-4: 0px !important; + --px-12: 2px !important; +} + html, body { margin: 0; From aff7c405cd391fd39b5b160623414f0b8f74d1b3 Mon Sep 17 00:00:00 2001 From: lohit Date: Wed, 20 Nov 2024 03:38:59 +0530 Subject: [PATCH 12/33] fix: import openapi -- baseUrl env value should not include trailing slash (#3440) * fix: openapi baseUrl env value should remove trailing slash * feat: updates --- packages/bruno-app/jest.config.js | 16 +++++ .../src/utils/importers/openapi-collection.js | 4 +- .../importers/openapi-collection.spec.js | 67 +++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 packages/bruno-app/jest.config.js create mode 100644 packages/bruno-app/src/utils/importers/openapi-collection.spec.js diff --git a/packages/bruno-app/jest.config.js b/packages/bruno-app/jest.config.js new file mode 100644 index 0000000000..5d94a67b78 --- /dev/null +++ b/packages/bruno-app/jest.config.js @@ -0,0 +1,16 @@ +module.exports = { + rootDir: '.', + moduleNameMapper: { + '^assets/(.*)$': '/src/assets/$1', + '^components/(.*)$': '/src/components/$1', + '^hooks/(.*)$': '/src/hooks/$1', + '^themes/(.*)$': '/src/themes/$1', + '^api/(.*)$': '/src/api/$1', + '^pageComponents/(.*)$': '/src/pageComponents/$1', + '^providers/(.*)$': '/src/providers/$1', + '^utils/(.*)$': '/src/utils/$1' + }, + clearMocks: true, + moduleDirectories: ['node_modules', 'src'], + testEnvironment: 'node' +}; diff --git a/packages/bruno-app/src/utils/importers/openapi-collection.js b/packages/bruno-app/src/utils/importers/openapi-collection.js index 01407878aa..9554290795 100644 --- a/packages/bruno-app/src/utils/importers/openapi-collection.js +++ b/packages/bruno-app/src/utils/importers/openapi-collection.js @@ -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(), diff --git a/packages/bruno-app/src/utils/importers/openapi-collection.spec.js b/packages/bruno-app/src/utils/importers/openapi-collection.spec.js new file mode 100644 index 0000000000..309221356c --- /dev/null +++ b/packages/bruno-app/src/utils/importers/openapi-collection.spec.js @@ -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); + }); +}); From 1cb0d4e1911a34158675a7ac0ad8fe255e09707d Mon Sep 17 00:00:00 2001 From: lohit Date: Wed, 20 Nov 2024 17:09:02 +0530 Subject: [PATCH 13/33] chore: node version bump -- v22.11.0 (#3508) node version bump with updates to cipher logic --- .nvmrc | 2 +- .../bruno-electron/src/utils/encryption.js | 56 ++++++++++++++++--- .../tests/utils/encryption.spec.js | 7 +++ 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/.nvmrc b/.nvmrc index 805b5a4e00..bb8c76c68e 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.9.0 +v22.11.0 diff --git a/packages/bruno-electron/src/utils/encryption.js b/packages/bruno-electron/src/utils/encryption.js index 89e33b1f9f..36c53e7ef6 100644 --- a/packages/bruno-electron/src/utils/encryption.js +++ b/packages/bruno-electron/src/utils/encryption.js @@ -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'); - - return decrypted; + const rawKey = machineIdSync(); + + // 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); diff --git a/packages/bruno-electron/tests/utils/encryption.spec.js b/packages/bruno-electron/tests/utils/encryption.spec.js index b7c9abcddc..44388fb07b 100644 --- a/packages/bruno-electron/tests/utils/encryption.spec.js +++ b/packages/bruno-electron/tests/utils/encryption.spec.js @@ -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'; From 412a0ed07862c7dd415290f0bb67e954767ee723 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:22:04 +0530 Subject: [PATCH 14/33] Now based on the request type appropriate views are shown. (#3340) * Now based on the request type appropriate views are shown. Co-authored-by: Anoop M D --- .../Sidebar/NewRequest/StyledWrapper.js | 8 ++ .../components/Sidebar/NewRequest/index.js | 89 +++++++++++++++++-- .../bruno-app/src/utils/codegenerator/har.js | 11 ++- .../bruno-app/src/utils/curl/curl-to-json.js | 14 ++- packages/bruno-app/src/utils/curl/index.js | 30 ++++++- 5 files changed, 138 insertions(+), 14 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js index 9845bd2efe..f7d7e914de 100644 --- a/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js index 48b871af31..f95b3efcc6 100644 --- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js +++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js @@ -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 ( +
+ {curlRequestTypeDetected === 'http-request' ? "HTTP" : "GraphQL"} + +
+ ); + }); + + // 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 ( @@ -279,15 +332,37 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { ) : (
- +
+ + } placement="bottom-end"> +
{ + dropdownTippyRef.current.hide(); + curlRequestTypeChange('http-request'); + }} + > + HTTP +
+
{ + dropdownTippyRef.current.hide(); + curlRequestTypeChange('graphql-request'); + }} + > + GraphQL +
+
+
{formik.touched.curlCommand && formik.errors.curlCommand ? (
{formik.errors.curlCommand}
diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index 9bbd0eea98..479fcd67a0 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -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 }); } diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js index e76f4014a7..44d9c4b2bc 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.js @@ -36,6 +36,12 @@ function getQueries(request) { return queries; } +/** + * Converts request data to a string based on its content type. + * + * @param {Object} request - The request object containing data and headers. + * @returns {Object} An object containing the data string. + */ function getDataString(request) { if (typeof request.data === 'number') { request.data = request.data.toString(); @@ -44,7 +50,13 @@ function getDataString(request) { const contentType = getContentType(request.headers); if (contentType && contentType.includes('application/json')) { - return { data: request.data.toString() }; + try { + const parsedData = JSON.parse(request.data); + return { data: JSON.stringify(parsedData) }; + } catch (error) { + console.error('Failed to parse JSON data:', error); + return { data: request.data.toString() }; + } } const parsedQueryString = querystring.parse(request.data, { sort: false }); diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js index e16dc68a56..e478a8e7e4 100644 --- a/packages/bruno-app/src/utils/curl/index.js +++ b/packages/bruno-app/src/utils/curl/index.js @@ -2,7 +2,7 @@ import { forOwn } from 'lodash'; import { convertToCodeMirrorJson } from 'utils/common'; import curlToJson from './curl-to-json'; -export const getRequestFromCurlCommand = (curlCommand) => { +export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-request') => { const parseFormData = (parsedBody) => { const formData = []; forOwn(parsedBody, (value, key) => { @@ -12,6 +12,22 @@ export const getRequestFromCurlCommand = (curlCommand) => { return formData; }; + const parseGraphQL = (text) => { + try { + const graphql = JSON.parse(text); + + return { + query: graphql.query, + variables: JSON.stringify(graphql.variables, null, 2) + }; + } catch (e) { + return { + query: '', + variables: '' + }; + } + }; + try { if (!curlCommand || typeof curlCommand !== 'string' || curlCommand.length === 0) { return null; @@ -24,6 +40,8 @@ export const getRequestFromCurlCommand = (curlCommand) => { Object.keys(parsedHeaders).map((key) => ({ name: key, value: parsedHeaders[key], enabled: true })); const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value; + const parsedBody = request.data; + const body = { mode: 'none', json: null, @@ -31,11 +49,15 @@ export const getRequestFromCurlCommand = (curlCommand) => { xml: null, sparql: null, multipartForm: null, - formUrlEncoded: null + formUrlEncoded: null, + graphql: null }; - const parsedBody = request.data; + if (parsedBody && contentType && typeof contentType === 'string') { - if (contentType.includes('application/json')) { + if (requestType === 'graphql-request' && (contentType.includes('application/json') || contentType.includes('application/graphql'))) { + body.mode = 'graphql'; + body.graphql = parseGraphQL(parsedBody); + } else if (contentType.includes('application/json')) { body.mode = 'json'; body.json = convertToCodeMirrorJson(parsedBody); } else if (contentType.includes('text/xml')) { From 77d3fa7e1e8b3938e6ac502f2e6c1776139a2118 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:34:00 +0530 Subject: [PATCH 15/33] bugfix / Update video preview functionality (#3433) * Add video preview functionality and update dependencies * Refactor video preview component to use Buffer for base64 decoding and update muted prop syntax --------- Co-authored-by: Anoop M D --- packages/bruno-app/package.json | 1 + .../QueryResult/QueryResultPreview/index.js | 33 ++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index f4d5fd0f82..52553b002e 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -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", diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js index 5683801ab1..6cb9ee2459 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js @@ -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 = 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
Loading video...
; + + return ( + console.error('Error loading video:', e)} + /> + ); +}); const QueryResultPreview = ({ previewTab, @@ -73,9 +100,7 @@ const QueryResultPreview = ({ ); } case 'preview-video': { - return ( -