diff --git a/package-lock.json b/package-lock.json index 951250897..a5294c5f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,6 @@ "@eslint/compat": "^1.3.2", "@faker-js/faker": "^7.6.0", "@jest/globals": "^29.2.0", - "@opencollection/types": "0.3.0", "@playwright/test": "^1.51.1", "@rollup/plugin-json": "^6.1.0", "@storybook/addon-webpack5-compiler-babel": "^4.0.0", @@ -6100,9 +6099,9 @@ } }, "node_modules/@opencollection/types": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.3.0.tgz", - "integrity": "sha512-kw+co3sM4ATDQI85lgy5UmOilEHFVdNYvOjZXmnSw6PUDUTAGBiaZNdwdSXwp//o4IwKbTQb8/0I3UdjLKh+qA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.5.0.tgz", + "integrity": "sha512-9rpu5agMrMLcMVU2UgyV+PYV3Zf/sHBJDHMQoq8XiMEUH8lt9f7yGtlerm/9dS3SHMpGX4A8ik0OFtc0dX4r1Q==", "dev": true, "license": "MIT" }, @@ -33627,7 +33626,7 @@ "devDependencies": { "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.4", - "@opencollection/types": "0.3.0", + "@opencollection/types": "~0.5.0", "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-node-resolve": "^15.0.1", @@ -35273,6 +35272,7 @@ "devDependencies": { "@babel/preset-env": "^7.22.0", "@babel/preset-typescript": "^7.22.0", + "@opencollection/types": "~0.5.0", "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.0.1", diff --git a/package.json b/package.json index fea21dd64..d31e4204f 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "@eslint/compat": "^1.3.2", "@faker-js/faker": "^7.6.0", "@jest/globals": "^29.2.0", - "@opencollection/types": "0.3.0", "@playwright/test": "^1.51.1", "@rollup/plugin-json": "^6.1.0", "@storybook/addon-webpack5-compiler-babel": "^4.0.0", diff --git a/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js b/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js index 5d45d5261..9ff12312c 100644 --- a/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js @@ -12,7 +12,19 @@ import Button from 'ui/Button'; const ProxySettings = ({ collection }) => { const dispatch = useDispatch(); - const initialProxyConfig = { enabled: 'global', protocol: 'http', hostname: '', port: '', auth: { enabled: false, username: '', password: '' }, bypassProxy: '' }; + const initialProxyConfig = { + inherit: true, + config: { + protocol: 'http', + hostname: '', + port: '', + auth: { + username: '', + password: '' + }, + bypassProxy: '' + } + }; // Get proxy from draft.brunoConfig if it exists, otherwise from brunoConfig const currentProxyConfig = collection.draft?.brunoConfig @@ -82,34 +94,57 @@ const ProxySettings = ({ collection }) => { const handleEnabledChange = (e) => { const value = e.target.value; - // Convert string to boolean or keep as 'global' - const enabled = value === 'true' ? true : value === 'false' ? false : 'global'; - updateProxy({ enabled }); + // Map UI values to new format + if (value === 'inherit') { + updateProxy({ disabled: false, inherit: true }); + } else if (value === 'true') { + updateProxy({ disabled: false, inherit: false }); + } else { + updateProxy({ disabled: true, inherit: false }); + } }; const handleProtocolChange = (e) => { - updateProxy({ protocol: e.target.value }); + updateProxy({ + config: { + ...currentProxyConfig.config, + protocol: e.target.value + } + }); }; const handleHostnameChange = (e) => { const hostname = e.target.value; if (validateHostnameOnChange(hostname)) { - updateProxy({ hostname }); + updateProxy({ + config: { + ...currentProxyConfig.config, + hostname + } + }); } }; const handlePortChange = (e) => { const port = e.target.value ? Number(e.target.value) : ''; if (validatePortOnChange(port)) { - updateProxy({ port }); + updateProxy({ + config: { + ...currentProxyConfig.config, + port + } + }); } }; const handleAuthEnabledChange = (e) => { updateProxy({ - auth: { - ...currentProxyConfig.auth, - enabled: e.target.checked + config: { + ...currentProxyConfig.config, + auth: { + ...currentProxyConfig.config.auth, + disabled: !e.target.checked + } } }); }; @@ -118,9 +153,12 @@ const ProxySettings = ({ collection }) => { const username = e.target.value; if (validateAuthUsernameOnChange(username)) { updateProxy({ - auth: { - ...currentProxyConfig.auth, - username + config: { + ...currentProxyConfig.config, + auth: { + ...currentProxyConfig.config.auth, + username + } } }); } @@ -130,9 +168,12 @@ const ProxySettings = ({ collection }) => { const password = e.target.value; if (validateAuthPasswordOnChange(password)) { updateProxy({ - auth: { - ...currentProxyConfig.auth, - password + config: { + ...currentProxyConfig.config, + auth: { + ...currentProxyConfig.config.auth, + password + } } }); } @@ -141,11 +182,19 @@ const ProxySettings = ({ collection }) => { const handleBypassProxyChange = (e) => { const bypassProxy = e.target.value; if (validateBypassProxyOnChange(bypassProxy)) { - updateProxy({ bypassProxy }); + updateProxy({ + config: { + ...currentProxyConfig.config, + bypassProxy + } + }); } }; - const enabledValue = currentProxyConfig.enabled === true ? 'true' : currentProxyConfig.enabled === false ? 'false' : 'global'; + // Map new format to UI values + const disabled = currentProxyConfig.disabled || false; + const inherit = currentProxyConfig.inherit !== undefined ? currentProxyConfig.inherit : true; + const enabledValue = disabled ? 'false' : (inherit ? 'inherit' : 'true'); return ( @@ -157,9 +206,9 @@ const ProxySettings = ({ collection }) => {
    -
  • global - use global proxy config
  • -
  • enabled - use collection proxy config
  • -
  • disable - disable proxy
  • +
  • inherit - inherit from global preferences
  • +
  • enabled - use collection-specific proxy config
  • +
  • disabled - disable proxy for this collection
@@ -169,12 +218,12 @@ const ProxySettings = ({ collection }) => { - global + inherit -
- -
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- -
- -
-
-
-
- - -
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+ + +
+
+
+
+ + +
+ + )}
+ Add Cookie +
) : cookies.length && !filteredCookies.length ? ( // No search results diff --git a/packages/bruno-app/src/components/Environments/Common/ExportEnvironmentModal/index.js b/packages/bruno-app/src/components/Environments/Common/ExportEnvironmentModal/index.js index 58f197b1c..963672ef9 100644 --- a/packages/bruno-app/src/components/Environments/Common/ExportEnvironmentModal/index.js +++ b/packages/bruno-app/src/components/Environments/Common/ExportEnvironmentModal/index.js @@ -6,6 +6,7 @@ import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions import { useDispatch } from 'react-redux'; import toast from 'react-hot-toast'; import StyledWrapper from './StyledWrapper'; +import Button from 'ui/Button'; const ExportEnvironmentModal = ({ onClose, environments = [], environmentType }) => { const dispatch = useDispatch(); @@ -242,22 +243,26 @@ const ExportEnvironmentModal = ({ onClose, environments = [], environmentType }) {/* Export Actions */}
- - +
diff --git a/packages/bruno-app/src/components/Preferences/ProxySettings/index.js b/packages/bruno-app/src/components/Preferences/ProxySettings/index.js index 3539b42dc..10bfc74a6 100644 --- a/packages/bruno-app/src/components/Preferences/ProxySettings/index.js +++ b/packages/bruno-app/src/components/Preferences/ProxySettings/index.js @@ -18,56 +18,41 @@ const ProxySettings = ({ close }) => { console.log(preferences); const proxySchema = Yup.object({ - mode: Yup.string().oneOf(['off', 'on', 'system']), - protocol: Yup.string().required().oneOf(['http', 'https', 'socks4', 'socks5']), - hostname: Yup.string() - .when('enabled', { - is: 'on', - then: (hostname) => hostname.required('Specify the hostname for your proxy.'), - otherwise: (hostname) => hostname.nullable() - }) - .max(1024), - port: Yup.number() - .min(1) - .max(65535) - .typeError('Specify port between 1 and 65535') - .nullable() - .transform((_, val) => (val ? Number(val) : null)), - auth: Yup.object() - .when('enabled', { - is: 'on', - then: Yup.object({ - enabled: Yup.boolean(), - username: Yup.string() - .when(['enabled'], { - is: true, - then: (username) => username.required('Specify username for proxy authentication.') - }) - .max(1024), - password: Yup.string() - .when('enabled', { - is: true, - then: (password) => password.required('Specify password for proxy authentication.') - }) - .max(1024) - }) - }) - .optional(), - bypassProxy: Yup.string().optional().max(1024) + disabled: Yup.boolean().optional(), + inherit: Yup.boolean().required(), + config: Yup.object({ + protocol: Yup.string().required().oneOf(['http', 'https', 'socks4', 'socks5']), + hostname: Yup.string().max(1024), + port: Yup.number() + .min(1) + .max(65535) + .typeError('Specify port between 1 and 65535') + .nullable() + .transform((_, val) => (val ? Number(val) : null)), + auth: Yup.object({ + disabled: Yup.boolean().optional(), + username: Yup.string().max(1024), + password: Yup.string().max(1024) + }).optional(), + bypassProxy: Yup.string().optional().max(1024) + }).required() }); const formik = useFormik({ initialValues: { - mode: preferences.proxy.mode, - protocol: preferences.proxy.protocol || 'http', - hostname: preferences.proxy.hostname || '', - port: preferences.proxy.port || 0, - auth: { - enabled: preferences.proxy.auth ? preferences.proxy.auth.enabled || false : false, - username: preferences.proxy.auth ? preferences.proxy.auth.username || '' : '', - password: preferences.proxy.auth ? preferences.proxy.auth.password || '' : '' - }, - bypassProxy: preferences.proxy.bypassProxy || '' + disabled: preferences.proxy.disabled || false, + inherit: preferences.proxy.inherit || false, + config: { + protocol: preferences.proxy.config?.protocol || 'http', + hostname: preferences.proxy.config?.hostname || '', + port: preferences.proxy.config?.port || 0, + auth: { + disabled: preferences.proxy.config?.auth?.disabled || false, + username: preferences.proxy.config?.auth?.username || '', + password: preferences.proxy.config?.auth?.password || '' + }, + bypassProxy: preferences.proxy.config?.bypassProxy || '' + } }, validationSchema: proxySchema, onSubmit: (values) => { @@ -103,16 +88,19 @@ const ProxySettings = ({ close }) => { useEffect(() => { formik.setValues({ - mode: preferences.proxy.mode, - protocol: preferences.proxy.protocol || 'http', - hostname: preferences.proxy.hostname || '', - port: preferences.proxy.port || '', - auth: { - enabled: preferences.proxy.auth ? preferences.proxy.auth.enabled || false : false, - username: preferences.proxy.auth ? preferences.proxy.auth.username || '' : '', - password: preferences.proxy.auth ? preferences.proxy.auth.password || '' : '' - }, - bypassProxy: preferences.proxy.bypassProxy || '' + disabled: preferences.proxy.disabled || false, + inherit: preferences.proxy.inherit || false, + config: { + protocol: preferences.proxy.config?.protocol || 'http', + hostname: preferences.proxy.config?.hostname || '', + port: preferences.proxy.config?.port || '', + auth: { + disabled: preferences.proxy.config?.auth?.disabled || false, + username: preferences.proxy.config?.auth?.username || '', + password: preferences.proxy.config?.auth?.password || '' + }, + bypassProxy: preferences.proxy.config?.bypassProxy || '' + } }); }, [preferences]); @@ -137,10 +125,11 @@ const ProxySettings = ({ close }) => { { - formik.setFieldValue('mode', 'off'); + formik.setFieldValue('disabled', true); + formik.setFieldValue('inherit', false); }} className="mr-1 cursor-pointer" /> @@ -150,10 +139,11 @@ const ProxySettings = ({ close }) => { { - formik.setFieldValue('mode', 'on'); + formik.setFieldValue('disabled', false); + formik.setFieldValue('inherit', false); }} className="mr-1 cursor-pointer" /> @@ -164,15 +154,18 @@ const ProxySettings = ({ close }) => { type="radio" name="mode" value="system" - checked={formik.values.mode === 'system'} - onChange={formik.handleChange} + checked={formik.values.disabled === false && formik.values.inherit === true} + onChange={(e) => { + formik.setFieldValue('disabled', false); + formik.setFieldValue('inherit', true); + }} className="mr-1 cursor-pointer" /> System Proxy - {formik?.values?.mode === 'system' ? ( + {formik.values.disabled === false && formik.values.inherit === true ? (
Below values are sourced from your system environment variables and cannot be directly updated in Bruno.
@@ -200,7 +193,7 @@ const ProxySettings = ({ close }) => {
) : null} - {formik?.values?.mode === 'on' ? ( + {formik.values.disabled === false && formik.values.inherit === false ? ( <>
-
-
-
-
-
-
diff --git a/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/GrpcurlModal/index.js b/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/GrpcurlModal/index.js index f57bbe5ff..75c17f5b7 100644 --- a/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/GrpcurlModal/index.js +++ b/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/GrpcurlModal/index.js @@ -6,6 +6,7 @@ import toast from 'react-hot-toast'; import get from 'lodash/get'; import Modal from 'components/Modal/index'; import CodeEditor from 'components/CodeEditor'; +import Button from 'ui/Button'; const GrpcurlModal = ({ isOpen, onClose, command }) => { const { displayedTheme } = useTheme(); @@ -39,12 +40,12 @@ const GrpcurlModal = ({ isOpen, onClose, command }) => {
- + icon={copied ? : } + />
{ const dispatch = useDispatch(); @@ -198,19 +199,12 @@ const CloneCollectionItem = ({ collectionUid, item, onClose }) => {
- - - - - - + +
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js index 0852c03d2..07dbd5558 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js @@ -15,6 +15,7 @@ import PathDisplay from 'components/PathDisplay'; import Portal from 'components/Portal'; import Dropdown from 'components/Dropdown'; import StyledWrapper from './StyledWrapper'; +import Button from 'ui/Button'; const RenameCollectionItem = ({ collectionUid, item, onClose }) => { const dispatch = useDispatch(); @@ -216,19 +217,12 @@ const RenameCollectionItem = ({ collectionUid, item, onClose }) => {
- - - - - - + +
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js index 12e0b1ea0..af0e0eea8 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js @@ -10,6 +10,7 @@ import StyledWrapper from './StyledWrapper'; import { areItemsLoading } from 'utils/collections'; import RunnerTags from 'components/RunnerResults/RunnerTags/index'; import { getRequestItemsForCollectionRun } from 'utils/collections/index'; +import Button from 'ui/Button'; const RunCollectionItem = ({ collectionUid, item, onClose }) => { const dispatch = useDispatch(); @@ -80,32 +81,24 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => {
- - - + { isCollectionRunInProgress ? ( - - - + ) : ( <> - - - - - - + + ) } diff --git a/packages/bruno-app/src/components/Sidebar/Collections/RemoveCollectionsModal/index.js b/packages/bruno-app/src/components/Sidebar/Collections/RemoveCollectionsModal/index.js index 088c7c761..611efff6f 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/RemoveCollectionsModal/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/RemoveCollectionsModal/index.js @@ -256,12 +256,12 @@ const RemoveCollectionsModal = ({ collectionUids, onClose }) => { Collections will be removed from the current workspace but will still be available in the file system and can be re-opened later.
- - + +
)} diff --git a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js index 3fa769b76..ec6a36aa3 100644 --- a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js +++ b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js @@ -13,6 +13,7 @@ import Help from 'components/Help'; import Dropdown from 'components/Dropdown'; import { IconCaretDown } from '@tabler/icons'; import StyledWrapper from './StyledWrapper'; +import Button from 'ui/Button'; const NewFolder = ({ collectionUid, item, onClose }) => { const dispatch = useDispatch(); @@ -181,19 +182,12 @@ const NewFolder = ({ collectionUid, item, onClose }) => {
- - - - - - + +
diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js index 2724c966d..401e044f4 100644 --- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js +++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js @@ -22,6 +22,7 @@ import Help from 'components/Help'; import StyledWrapper from './StyledWrapper'; import SingleLineEditor from 'components/SingleLineEditor/index'; import { useTheme } from 'styled-components'; +import Button from 'ui/Button'; const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => { const dispatch = useDispatch(); @@ -594,16 +595,12 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
- - - - - - + +
diff --git a/packages/bruno-converters/package.json b/packages/bruno-converters/package.json index 1331fdca9..326bbc97e 100644 --- a/packages/bruno-converters/package.json +++ b/packages/bruno-converters/package.json @@ -28,12 +28,12 @@ "devDependencies": { "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.4", - "@opencollection/types": "0.3.0", - "@usebruno/schema-types": "0.0.1", + "@opencollection/types": "~0.5.0", "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^9.0.2", + "@usebruno/schema-types": "0.0.1", "@web/rollup-plugin-copy": "^0.5.1", "babel-jest": "^29.7.0", "rimraf": "^5.0.7", diff --git a/packages/bruno-converters/src/opencollection/bruno-to-opencollection.ts b/packages/bruno-converters/src/opencollection/bruno-to-opencollection.ts index 64d168a16..159c8566a 100644 --- a/packages/bruno-converters/src/opencollection/bruno-to-opencollection.ts +++ b/packages/bruno-converters/src/opencollection/bruno-to-opencollection.ts @@ -32,27 +32,12 @@ const toOpenCollectionConfig = (brunoConfig: BrunoConfig | undefined): Collectio } } - if (brunoConfig.proxy?.enabled) { - if (brunoConfig.proxy.enabled === 'global') { - config.proxy = 'inherit'; - } else { - config.proxy = { - protocol: brunoConfig.proxy.protocol || 'http', - hostname: brunoConfig.proxy.hostname || '', - port: brunoConfig.proxy.port || 0 - }; - - if (brunoConfig.proxy.auth?.enabled) { - config.proxy.auth = { - username: brunoConfig.proxy.auth.username || '', - password: brunoConfig.proxy.auth.password || '' - }; - } - - if (brunoConfig.proxy.bypassProxy) { - config.proxy.bypassProxy = brunoConfig.proxy.bypassProxy; - } - } + if (brunoConfig.proxy) { + config.proxy = { + disabled: brunoConfig.proxy.disabled, + inherit: brunoConfig.proxy.inherit, + config: brunoConfig.proxy.config + }; } if (brunoConfig.clientCertificates?.certs?.length) { diff --git a/packages/bruno-converters/src/opencollection/common/auth.ts b/packages/bruno-converters/src/opencollection/common/auth.ts index 9403692dd..3266f434e 100644 --- a/packages/bruno-converters/src/opencollection/common/auth.ts +++ b/packages/bruno-converters/src/opencollection/common/auth.ts @@ -124,7 +124,7 @@ const fromOpenCollectionOAuth2 = (auth: AuthOAuth2): BrunoAuth => { clientId: auth.credentials?.clientId || null, clientSecret: auth.credentials?.clientSecret || null, scope: auth.scope || null, - pkce: auth.pkce?.enabled || null, + pkce: (auth.pkce && !auth.pkce.disabled) || null, credentialsPlacement: getCredentialsPlacement(auth.credentials), credentialsId: auth.tokenConfig?.id || 'credentials', tokenPlacement: getTokenPlacement(auth.tokenConfig), @@ -357,7 +357,7 @@ const toOpenCollectionOAuth2 = (oauth2: BrunoOAuth2 | null | undefined): AuthOAu placement: oauth2.credentialsPlacement === 'basic_auth_header' ? 'basic_auth_header' : 'body' }, scope: oauth2.scope || '', - pkce: oauth2.pkce ? { enabled: true, method: 'S256' } : undefined, + pkce: oauth2.pkce ? { method: 'S256' } : undefined, tokenConfig: { id: oauth2.credentialsId || 'credentials', placement: oauth2.tokenPlacement === 'query' diff --git a/packages/bruno-converters/src/opencollection/opencollection-to-bruno.ts b/packages/bruno-converters/src/opencollection/opencollection-to-bruno.ts index f6ffba99e..89e187748 100644 --- a/packages/bruno-converters/src/opencollection/opencollection-to-bruno.ts +++ b/packages/bruno-converters/src/opencollection/opencollection-to-bruno.ts @@ -36,30 +36,12 @@ const fromOpenCollectionConfig = (oc: OpenCollection): BrunoConfig => { }; } - if (config.proxy && typeof config.proxy !== 'boolean') { - if (config.proxy === 'inherit') { - brunoConfig.proxy = { enabled: 'global' }; - } else { - const proxyConfig = config.proxy; - brunoConfig.proxy = { - enabled: true, - protocol: proxyConfig.protocol || 'http', - hostname: proxyConfig.hostname || '', - port: proxyConfig.port || 0 - }; - - if (proxyConfig.auth) { - brunoConfig.proxy.auth = { - enabled: true, - username: proxyConfig.auth.username || '', - password: proxyConfig.auth.password || '' - }; - } - - if (proxyConfig.bypassProxy) { - brunoConfig.proxy.bypassProxy = proxyConfig.bypassProxy; - } - } + if (config.proxy) { + brunoConfig.proxy = { + disabled: config.proxy.disabled, + inherit: config.proxy.inherit, + config: config.proxy.config + }; } if (config.clientCertificates?.length) { diff --git a/packages/bruno-converters/src/opencollection/types.ts b/packages/bruno-converters/src/opencollection/types.ts index 13ba07c0a..34c9492e7 100644 --- a/packages/bruno-converters/src/opencollection/types.ts +++ b/packages/bruno-converters/src/opencollection/types.ts @@ -66,9 +66,10 @@ export type { } from '@opencollection/types/requests/websocket'; // OpenCollection config types -export type { Environment, CollectionConfig } from '@opencollection/types/config/environments'; +export type { Environment } from '@opencollection/types/config/environments'; +export type { CollectionConfig } from '@opencollection/types/config/collection'; export type { Protobuf, ProtoFileItem, ProtoFileImportPath } from '@opencollection/types/config/protobuf'; -export type { Proxy, ProxyAuth } from '@opencollection/types/config/proxy'; +export type { Proxy, ProxyConnectionConfig, ProxyConnectionAuth } from '@opencollection/types/config/proxy'; export type { ClientCertificate, PemCertificate, Pkcs12Certificate } from '@opencollection/types/config/certificates'; // OpenCollection common types @@ -175,16 +176,19 @@ export interface BrunoConfig { importPaths?: { path: string; disabled?: boolean }[]; }; proxy?: { - enabled?: boolean | 'global'; - protocol?: string; - hostname?: string; - port?: number; - auth?: { - enabled?: boolean; - username?: string; - password?: string; + disabled?: boolean; + inherit?: boolean; + config?: { + protocol?: string; + hostname?: string; + port?: number; + auth?: { + disabled?: boolean; + username?: string; + password?: string; + }; + bypassProxy?: string; }; - bypassProxy?: string; }; clientCertificates?: { certs?: Array<{ diff --git a/packages/bruno-electron/src/app/collection-watcher.js b/packages/bruno-electron/src/app/collection-watcher.js index 036fc7414..52a2998aa 100644 --- a/packages/bruno-electron/src/app/collection-watcher.js +++ b/packages/bruno-electron/src/app/collection-watcher.js @@ -27,7 +27,7 @@ const EnvironmentSecretsStore = require('../store/env-secrets'); const UiStateSnapshot = require('../store/ui-state-snapshot'); const { parseFileMeta, hydrateRequestWithUuid } = require('../utils/collection'); const { parseLargeRequestWithRedaction } = require('../utils/parse'); -const { transformBrunoConfigAfterRead } = require('../utils/transfomBrunoConfig'); +const { transformBrunoConfigAfterRead } = require('../utils/transformBrunoConfig'); const MAX_FILE_SIZE = 2.5 * 1024 * 1024; diff --git a/packages/bruno-electron/src/app/collections.js b/packages/bruno-electron/src/app/collections.js index ed9388b69..db7c7e3d0 100644 --- a/packages/bruno-electron/src/app/collections.js +++ b/packages/bruno-electron/src/app/collections.js @@ -4,7 +4,7 @@ const { dialog, ipcMain } = require('electron'); const Yup = require('yup'); const { isDirectory, getCollectionStats, normalizeAndResolvePath } = require('../utils/filesystem'); const { generateUidBasedOnHash } = require('../utils/common'); -const { transformBrunoConfigAfterRead } = require('../utils/transfomBrunoConfig'); +const { transformBrunoConfigAfterRead } = require('../utils/transformBrunoConfig'); const { parseCollection } = require('@usebruno/filestore'); // todo: bruno.json config schema validation errors must be propagated to the UI diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 5481ca911..768556354 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -62,7 +62,7 @@ const { getProcessEnvVars } = require('../store/process-env'); const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant, refreshOauth2Token } = require('../utils/oauth2'); const { getCertsAndProxyConfig } = require('./network/cert-utils'); const collectionWatcher = require('../app/collection-watcher'); -const { transformBrunoConfigBeforeSave } = require('../utils/transfomBrunoConfig'); +const { transformBrunoConfigBeforeSave } = require('../utils/transformBrunoConfig'); const { REQUEST_TYPES } = require('../utils/constants'); const { cancelOAuth2AuthorizationRequest, isOauth2AuthorizationRequestInProgress } = require('../utils/oauth2-protocol-handler'); diff --git a/packages/bruno-electron/src/ipc/network/cert-utils.js b/packages/bruno-electron/src/ipc/network/cert-utils.js index 703caec97..4a285a76c 100644 --- a/packages/bruno-electron/src/ipc/network/cert-utils.js +++ b/packages/bruno-electron/src/ipc/network/cert-utils.js @@ -101,12 +101,14 @@ const getCertsAndProxyConfig = async ({ /** * Proxy configuration * - * Preferences proxyMode has three possible values: on, off, system - * Collection proxyMode has three possible values: true, false, global + * New format: + * - disabled: boolean (optional, defaults to false) + * - inherit: boolean (required) + * - config: { protocol, hostname, port, auth, bypassProxy } * - * When collection proxyMode is true, it overrides the app-level proxy settings - * When collection proxyMode is false, it ignores the app-level proxy settings - * When collection proxyMode is global, it uses the app-level proxy settings + * When collection proxy has inherit=false and disabled=false, use collection-specific proxy + * When collection proxy has inherit=true, inherit from global preferences + * When disabled=true, proxy is disabled * * Below logic calculates the proxyMode and proxyConfig to be used for the request */ @@ -114,14 +116,32 @@ const getCertsAndProxyConfig = async ({ let proxyConfig = {}; const collectionProxyConfig = get(brunoConfig, 'proxy', {}); - const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', 'global'); - if (collectionProxyEnabled === true) { - proxyConfig = collectionProxyConfig; + const collectionProxyDisabled = get(collectionProxyConfig, 'disabled', false); + const collectionProxyInherit = get(collectionProxyConfig, 'inherit', true); + const collectionProxyConfigData = get(collectionProxyConfig, 'config', collectionProxyConfig); + + if (!collectionProxyDisabled && !collectionProxyInherit) { + // Use collection-specific proxy + proxyConfig = collectionProxyConfigData; proxyMode = 'on'; - } else if (collectionProxyEnabled === 'global') { - proxyConfig = preferencesUtil.getGlobalProxyConfig(); - proxyMode = get(proxyConfig, 'mode', 'off'); + } else if (!collectionProxyDisabled && collectionProxyInherit) { + // Inherit from global preferences + const globalProxy = preferencesUtil.getGlobalProxyConfig(); + const globalDisabled = get(globalProxy, 'disabled', false); + const globalInherit = get(globalProxy, 'inherit', false); + const globalProxyConfigData = get(globalProxy, 'config', globalProxy); + + if (!globalDisabled && !globalInherit) { + // Use global custom proxy + proxyConfig = globalProxyConfigData; + proxyMode = 'on'; + } else if (!globalDisabled && globalInherit) { + // Use system proxy + proxyMode = 'system'; + } + // else: global proxy is disabled, proxyMode stays 'off' } + // else: collection proxy is disabled, proxyMode stays 'off' return { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions }; }; diff --git a/packages/bruno-electron/src/store/preferences.js b/packages/bruno-electron/src/store/preferences.js index 53a722a33..7673fdf0f 100644 --- a/packages/bruno-electron/src/store/preferences.js +++ b/packages/bruno-electron/src/store/preferences.js @@ -30,16 +30,17 @@ const defaultPreferences = { codeFontSize: 13 }, proxy: { - mode: 'off', - protocol: 'http', - hostname: '', - port: null, - auth: { - enabled: false, - username: '', - password: '' - }, - bypassProxy: '' + inherit: true, + config: { + protocol: 'http', + hostname: '', + port: null, + auth: { + username: '', + password: '' + }, + bypassProxy: '' + } }, layout: { responsePaneOrientation: 'horizontal' @@ -79,16 +80,19 @@ const preferencesSchema = Yup.object().shape({ codeFontSize: Yup.number().min(1).max(32).nullable() }), proxy: Yup.object({ - mode: Yup.string().oneOf(['off', 'on', 'system']), - protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']), - hostname: Yup.string().max(1024), - port: Yup.number().min(1).max(65535).nullable(), - auth: Yup.object({ - enabled: Yup.boolean(), - username: Yup.string().max(1024), - password: Yup.string().max(1024) - }).optional(), - bypassProxy: Yup.string().optional().max(1024) + disabled: Yup.boolean().optional(), + inherit: Yup.boolean().required(), + config: Yup.object({ + protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']), + hostname: Yup.string().max(1024), + port: Yup.number().min(1).max(65535).nullable(), + auth: Yup.object({ + disabled: Yup.boolean().optional(), + username: Yup.string().max(1024), + password: Yup.string().max(1024) + }).optional(), + bypassProxy: Yup.string().optional().max(1024) + }).required() }), layout: Yup.object({ responsePaneOrientation: Yup.string().oneOf(['horizontal', 'vertical']) @@ -118,17 +122,73 @@ class PreferencesStore { getPreferences() { let preferences = this.store.get('preferences', {}); - // This to support the old preferences format - // In the old format, we had a proxy.enabled flag - // In the new format, this maps to proxy.mode = 'on' - if (preferences?.proxy?.enabled) { - preferences.proxy.mode = 'on'; - } + // Migrate proxy configuration from old formats to new format + const proxyMigrated = get(preferences, '_migrations.proxyConfigFormat', false); + if (!proxyMigrated && preferences?.proxy) { + const proxy = preferences.proxy || {}; - // Delete the proxy.enabled property if it exists, regardless of its value - // This is a part of migration to the new preferences format - if (preferences?.proxy && 'enabled' in preferences.proxy) { - delete preferences.proxy.enabled; + // Check if this is an old format that needs migration + const hasOldFormat = proxy.hasOwnProperty('enabled') || proxy.hasOwnProperty('mode'); + + if (hasOldFormat) { + let newProxy = { + inherit: true, + config: { + protocol: proxy.protocol || 'http', + hostname: proxy.hostname || '', + port: proxy.port || null, + auth: { + username: get(proxy, 'auth.username', ''), + password: get(proxy, 'auth.password', '') + }, + bypassProxy: proxy.bypassProxy || '' + } + }; + + // Handle old format 1: enabled (boolean) + if (proxy.hasOwnProperty('enabled') && typeof proxy.enabled === 'boolean') { + newProxy.disabled = !proxy.enabled; + newProxy.inherit = false; + } else if (proxy.hasOwnProperty('mode')) { + // Handle old format 2: mode ('off' | 'on' | 'system') + if (proxy.mode === 'off') { + newProxy.disabled = true; + newProxy.inherit = false; + } else if (proxy.mode === 'on') { + newProxy.disabled = false; + newProxy.inherit = false; + } else if (proxy.mode === 'system') { + newProxy.disabled = false; + newProxy.inherit = true; + } + } + + // Migrate auth.enabled to auth.disabled + if (get(proxy, 'auth.enabled') === false) { + newProxy.config.auth.disabled = true; + } + // If auth.enabled is true or undefined, omit disabled (defaults to false) + + // Omit disabled: false at top level (optional field) + if (newProxy.disabled === false) { + delete newProxy.disabled; + } + // Omit auth.disabled: false (optional field) + if (newProxy.config.auth.disabled === false) { + delete newProxy.config.auth.disabled; + } + + preferences.proxy = newProxy; + + // Mark migration as complete // ? + // if (!preferences._migrations) { + // preferences._migrations = {}; + // } + // preferences._migrations.proxyConfigFormat = true; + + // Save the migrated preferences back to the store + // this.store.set('preferences', preferences); + } } // Migrate font size from 14px to 13px for existing users diff --git a/packages/bruno-electron/src/utils/proxy-util.js b/packages/bruno-electron/src/utils/proxy-util.js index dacbfa98f..06e5c90d2 100644 --- a/packages/bruno-electron/src/utils/proxy-util.js +++ b/packages/bruno-electron/src/utils/proxy-util.js @@ -318,7 +318,8 @@ function setupProxyAgents({ const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions); const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions); const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions); - const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false); + const proxyAuthDisabled = get(proxyConfig, 'auth.disabled', false); + const proxyAuthEnabled = !proxyAuthDisabled; const socksEnabled = proxyProtocol.includes('socks'); let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`; diff --git a/packages/bruno-electron/src/utils/transfomBrunoConfig.js b/packages/bruno-electron/src/utils/transformBrunoConfig.js similarity index 50% rename from packages/bruno-electron/src/utils/transfomBrunoConfig.js rename to packages/bruno-electron/src/utils/transformBrunoConfig.js index 8b3ffb763..43a5e21d3 100644 --- a/packages/bruno-electron/src/utils/transfomBrunoConfig.js +++ b/packages/bruno-electron/src/utils/transformBrunoConfig.js @@ -1,5 +1,6 @@ const path = require('path'); const { isFile, isDirectory } = require('./filesystem'); +const { get } = require('lodash'); function transformBrunoConfigBeforeSave(brunoConfig) { // remove exists from importPaths and protoFiles @@ -16,6 +17,18 @@ function transformBrunoConfigBeforeSave(brunoConfig) { }); } + // Clean up proxy config before saving + if (brunoConfig.proxy) { + // Remove disabled: false (optional field) + if (brunoConfig.proxy.disabled === false) { + delete brunoConfig.proxy.disabled; + } + // Remove auth.disabled: false (optional field) + if (brunoConfig.proxy.config?.auth?.disabled === false) { + delete brunoConfig.proxy.config.auth.disabled; + } + } + return brunoConfig; } @@ -61,6 +74,59 @@ async function transformBrunoConfigAfterRead(brunoConfig, collectionPathname) { })); } + // Migrate proxy configuration from old format to new format + if (brunoConfig.proxy) { + const proxy = brunoConfig.proxy || {}; + + // Check if this is an old format (has 'enabled' property) + if (proxy.hasOwnProperty('enabled')) { + const enabled = proxy.enabled; + + let newProxy = { + inherit: true, + config: { + protocol: proxy.protocol || 'http', + hostname: proxy.hostname || '', + port: proxy.port || null, + auth: { + username: get(proxy, 'auth.username', ''), + password: get(proxy, 'auth.password', '') + }, + bypassProxy: proxy.bypassProxy || '' + } + }; + + // Handle old format: enabled (true | false | 'global') + if (enabled === true) { + newProxy.disabled = false; + newProxy.inherit = false; + } else if (enabled === false) { + newProxy.disabled = true; + newProxy.inherit = false; + } else if (enabled === 'global') { + newProxy.disabled = false; + newProxy.inherit = true; + } + + // Migrate auth.enabled to auth.disabled + if (get(proxy, 'auth.enabled') === false) { + newProxy.config.auth.disabled = true; + } + // If auth.enabled is true or undefined, omit disabled (defaults to false) + + // Omit disabled: false at top level (optional field) + if (newProxy.disabled === false) { + delete newProxy.disabled; + } + // Omit auth.disabled: false (optional field) + if (newProxy.config.auth.disabled === false) { + delete newProxy.config.auth.disabled; + } + + brunoConfig.proxy = newProxy; + } + } + return brunoConfig; } diff --git a/packages/bruno-electron/tests/store/proxy-preferences.spec.js b/packages/bruno-electron/tests/store/proxy-preferences.spec.js new file mode 100644 index 000000000..cc202c9b3 --- /dev/null +++ b/packages/bruno-electron/tests/store/proxy-preferences.spec.js @@ -0,0 +1,297 @@ +let mockStoreData = {}; + +jest.mock('electron-store', () => { + return jest.fn().mockImplementation((opts = {}) => { + return { + get: (key, fallback) => (key in mockStoreData ? mockStoreData[key] : fallback), + set: (key, value) => { + mockStoreData[key] = value; + } + }; + }); +}); + +const { getPreferences, savePreferences } = require('../../src/store/preferences'); + +describe('Proxy Preferences Migration', () => { + beforeEach(() => { + // Reset mock store data before each test + mockStoreData = {}; + }); + + describe('New Format (no migration needed)', () => { + it('should handle new format with inherit: false', () => { + const newFormatProxy = { + proxy: { + inherit: false, + config: { + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + username: 'user', + password: 'pass' + }, + bypassProxy: 'localhost' + } + } + }; + + mockStoreData['preferences'] = newFormatProxy; + + const preferences = getPreferences(); + + // Verify key fields are preserved from stored preferences + expect(preferences.proxy.inherit).toBe(false); + expect(preferences.proxy.config.protocol).toBe('http'); + expect(preferences.proxy.config.hostname).toBe('proxy.example.com'); + expect(preferences.proxy.config.port).toBe(8080); + expect(preferences.proxy.config.auth.username).toBe('user'); + expect(preferences.proxy.config.auth.password).toBe('pass'); + expect(preferences.proxy.config.bypassProxy).toBe('localhost'); + }); + + it('should handle new format with inherit: true', () => { + const newFormatProxy = { + proxy: { + inherit: true, + config: { + protocol: 'http', + hostname: '', + port: null, + auth: { + username: '', + password: '' + }, + bypassProxy: '' + } + } + }; + + mockStoreData['preferences'] = newFormatProxy; + + const preferences = getPreferences(); + + expect(preferences.proxy.inherit).toBe(true); + expect(preferences.proxy.config).toBeDefined(); + }); + + it('should handle new format with disabled: true', () => { + const newFormatProxy = { + proxy: { + disabled: true, + inherit: false, + config: { + protocol: 'http', + hostname: '', + port: null, + auth: { + username: '', + password: '' + }, + bypassProxy: '' + } + } + }; + + mockStoreData['preferences'] = newFormatProxy; + + const preferences = getPreferences(); + + // disabled: true is preserved from stored preferences + expect(preferences.proxy.disabled).toBe(true); + expect(preferences.proxy.inherit).toBe(false); + expect(preferences.proxy.config).toBeDefined(); + }); + + it('should handle new format with auth.disabled: true', () => { + const newFormatProxy = { + proxy: { + inherit: false, + config: { + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + disabled: true, + username: 'user', + password: 'pass' + }, + bypassProxy: '' + } + } + }; + + mockStoreData['preferences'] = newFormatProxy; + + const preferences = getPreferences(); + + // auth.disabled: true is preserved from stored preferences + expect(preferences.proxy.config.auth.disabled).toBe(true); + expect(preferences.proxy.config.auth.username).toBe('user'); + expect(preferences.proxy.config.auth.password).toBe('pass'); + }); + }); + + describe('Old Format 1: enabled (boolean)', () => { + it('should migrate enabled: true to disabled: false, inherit: false', () => { + const oldFormatProxy = { + proxy: { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + enabled: true, + username: 'user', + password: 'pass' + }, + bypassProxy: 'localhost' + } + }; + + mockStoreData['preferences'] = oldFormatProxy; + + const preferences = getPreferences(); + + // After migration, inherit should be false (old enabled: true maps to inherit: false) + expect(preferences.proxy.inherit).toBe(false); + // Values are preserved from stored preferences + expect(preferences.proxy.config.protocol).toBe('http'); + expect(preferences.proxy.config.hostname).toBe('proxy.example.com'); + expect(preferences.proxy.config.port).toBe(8080); + expect(preferences.proxy.config.auth.username).toBe('user'); + expect(preferences.proxy.config.auth.password).toBe('pass'); + expect(preferences.proxy.config.bypassProxy).toBe('localhost'); + expect(preferences.proxy.disabled).toBeUndefined(); // disabled: false is omitted + }); + + it('should migrate enabled: false to disabled: true, inherit: false', () => { + const oldFormatProxy = { + proxy: { + enabled: false, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + enabled: false, + username: '', + password: '' + }, + bypassProxy: '' + } + }; + + mockStoreData['preferences'] = oldFormatProxy; + + const preferences = getPreferences(); + + // After migration, enabled: false becomes disabled: true, inherit: false + expect(preferences.proxy.disabled).toBe(true); + expect(preferences.proxy.inherit).toBe(false); + }); + + it('should migrate auth.enabled: false to auth.disabled: true', () => { + const oldFormatProxy = { + proxy: { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + enabled: false, + username: 'user', + password: 'pass' + }, + bypassProxy: '' + } + }; + + mockStoreData['preferences'] = oldFormatProxy; + + const preferences = getPreferences(); + + // auth.disabled: true is preserved from stored preferences + expect(preferences.proxy.config.auth.disabled).toBe(true); + }); + }); + + describe('Old Format 2: mode (string)', () => { + it('should migrate mode: "off" to disabled: true, inherit: false', () => { + const oldFormatProxy = { + proxy: { + mode: 'off', + protocol: 'http', + hostname: '', + port: null, + auth: { + enabled: false, + username: '', + password: '' + }, + bypassProxy: '' + } + }; + + mockStoreData['preferences'] = oldFormatProxy; + + const preferences = getPreferences(); + + // disabled: true is preserved from migration + expect(preferences.proxy.disabled).toBe(true); + expect(preferences.proxy.inherit).toBe(false); + }); + + it('should migrate mode: "on" to disabled: false, inherit: false', () => { + const oldFormatProxy = { + proxy: { + mode: 'on', + protocol: 'https', + hostname: 'proxy.example.com', + port: 8443, + auth: { + enabled: true, + username: 'user', + password: 'pass' + }, + bypassProxy: '*.local' + } + }; + + mockStoreData['preferences'] = oldFormatProxy; + + const preferences = getPreferences(); + + expect(preferences.proxy.disabled).toBeUndefined(); // disabled: false is omitted + expect(preferences.proxy.inherit).toBe(false); + // Values are preserved from stored preferences + expect(preferences.proxy.config.protocol).toBe('https'); + expect(preferences.proxy.config.hostname).toBe('proxy.example.com'); + expect(preferences.proxy.config.port).toBe(8443); + }); + + it('should migrate mode: "system" to disabled: false, inherit: true', () => { + const oldFormatProxy = { + proxy: { + mode: 'system', + protocol: 'http', + hostname: '', + port: null, + auth: { + enabled: false, + username: '', + password: '' + }, + bypassProxy: '' + } + }; + + mockStoreData['preferences'] = oldFormatProxy; + + const preferences = getPreferences(); + + expect(preferences.proxy.disabled).toBeUndefined(); // disabled: false is omitted + expect(preferences.proxy.inherit).toBe(true); + }); + }); +}); diff --git a/packages/bruno-electron/tests/utils/transform-bruno-config.spec.js b/packages/bruno-electron/tests/utils/transform-bruno-config.spec.js new file mode 100644 index 000000000..98901f03e --- /dev/null +++ b/packages/bruno-electron/tests/utils/transform-bruno-config.spec.js @@ -0,0 +1,570 @@ +const { transformBrunoConfigBeforeSave, transformBrunoConfigAfterRead } = require('../../src/utils/transformBrunoConfig'); + +describe('BrunoConfig Proxy Transform', () => { + describe('transformBrunoConfigAfterRead - Migration from old to new format', () => { + describe('Old Format: enabled (true | false | "global")', () => { + test('should migrate enabled: true to disabled: false, inherit: false', async () => { + const oldConfig = { + name: 'Test Collection', + proxy: { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + enabled: true, + username: 'user', + password: 'pass' + }, + bypassProxy: 'localhost' + } + }; + + const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path'); + + expect(result.proxy).toEqual({ + inherit: false, + config: { + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + username: 'user', + password: 'pass' + }, + bypassProxy: 'localhost' + } + }); + expect(result.proxy.disabled).toBeUndefined(); // disabled: false is omitted + }); + + test('should migrate enabled: false to disabled: true, inherit: false', async () => { + const oldConfig = { + name: 'Test Collection', + proxy: { + enabled: false, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + enabled: false, + username: '', + password: '' + }, + bypassProxy: '' + } + }; + + const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path'); + + expect(result.proxy.disabled).toBe(true); + expect(result.proxy.inherit).toBe(false); + }); + + test('should migrate enabled: "global" to disabled: false, inherit: true', async () => { + const oldConfig = { + name: 'Test Collection', + proxy: { + enabled: 'global', + protocol: 'http', + hostname: '', + port: null, + auth: { + enabled: false, + username: '', + password: '' + }, + bypassProxy: '' + } + }; + + const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path'); + + expect(result.proxy.disabled).toBeUndefined(); // disabled: false is omitted + expect(result.proxy.inherit).toBe(true); + }); + + test('should migrate auth.enabled: false to auth.disabled: true', async () => { + const oldConfig = { + name: 'Test Collection', + proxy: { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + enabled: false, + username: 'user', + password: 'pass' + }, + bypassProxy: '' + } + }; + + const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path'); + + expect(result.proxy.config.auth.disabled).toBe(true); + expect(result.proxy.config.auth.username).toBe('user'); + expect(result.proxy.config.auth.password).toBe('pass'); + }); + + test('should omit auth.disabled when auth.enabled: true', async () => { + const oldConfig = { + name: 'Test Collection', + proxy: { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + enabled: true, + username: 'user', + password: 'pass' + }, + bypassProxy: '' + } + }; + + const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path'); + + expect(result.proxy.config.auth.disabled).toBeUndefined(); + expect(result.proxy.config.auth.username).toBe('user'); + expect(result.proxy.config.auth.password).toBe('pass'); + }); + }); + + describe('New Format (no migration)', () => { + test('should not modify new format with inherit: false', async () => { + const newConfig = { + name: 'Test Collection', + proxy: { + inherit: false, + config: { + protocol: 'https', + hostname: 'proxy.example.com', + port: 8443, + auth: { + username: 'user', + password: 'pass' + }, + bypassProxy: '*.local' + } + } + }; + + const result = await transformBrunoConfigAfterRead(newConfig, '/test/path'); + + expect(result.proxy).toEqual(newConfig.proxy); + }); + + test('should not modify new format with inherit: true', async () => { + const newConfig = { + name: 'Test Collection', + proxy: { + inherit: true, + config: { + protocol: 'http', + hostname: '', + port: null, + auth: { + username: '', + password: '' + }, + bypassProxy: '' + } + } + }; + + const result = await transformBrunoConfigAfterRead(newConfig, '/test/path'); + + expect(result.proxy).toEqual(newConfig.proxy); + }); + + test('should not modify new format with disabled: true', async () => { + const newConfig = { + name: 'Test Collection', + proxy: { + disabled: true, + inherit: false, + config: { + protocol: 'http', + hostname: '', + port: null, + auth: { + username: '', + password: '' + }, + bypassProxy: '' + } + } + }; + + const result = await transformBrunoConfigAfterRead(newConfig, '/test/path'); + + expect(result.proxy).toEqual(newConfig.proxy); + }); + + test('should not modify new format with auth.disabled: true', async () => { + const newConfig = { + name: 'Test Collection', + proxy: { + inherit: false, + config: { + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + disabled: true, + username: 'user', + password: 'pass' + }, + bypassProxy: '' + } + } + }; + + const result = await transformBrunoConfigAfterRead(newConfig, '/test/path'); + + expect(result.proxy).toEqual(newConfig.proxy); + }); + }); + + describe('Edge Cases', () => { + test('should handle missing proxy config', async () => { + const config = { + name: 'Test Collection' + }; + + const result = await transformBrunoConfigAfterRead(config, '/test/path'); + + expect(result.proxy).toBeUndefined(); + }); + + test('should handle null port values', async () => { + const oldConfig = { + name: 'Test Collection', + proxy: { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: null, + auth: { + enabled: false, + username: '', + password: '' + }, + bypassProxy: '' + } + }; + + const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path'); + + expect(result.proxy.config.port).toBeNull(); + }); + + test('should handle SOCKS protocols', async () => { + const oldConfig = { + name: 'Test Collection', + proxy: { + enabled: true, + protocol: 'socks5', + hostname: 'socks.example.com', + port: 1080, + auth: { + enabled: true, + username: 'socksuser', + password: 'sockspass' + }, + bypassProxy: '' + } + }; + + const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path'); + + expect(result.proxy.config.protocol).toBe('socks5'); + expect(result.proxy.config.hostname).toBe('socks.example.com'); + expect(result.proxy.config.port).toBe(1080); + }); + + test('should handle missing auth object', async () => { + const oldConfig = { + name: 'Test Collection', + proxy: { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + bypassProxy: '' + } + }; + + const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path'); + + expect(result.proxy.config.auth).toEqual({ + username: '', + password: '' + }); + }); + + test('should preserve protobuf config during proxy migration', async () => { + const oldConfig = { + name: 'Test Collection', + protobuf: { + protoFiles: [{ path: 'test.proto' }], + importPaths: [{ path: 'imports/' }] + }, + proxy: { + enabled: 'global', + protocol: 'http', + hostname: '', + port: null, + auth: { + enabled: false, + username: '', + password: '' + }, + bypassProxy: '' + } + }; + + const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path'); + + expect(result.protobuf).toBeDefined(); + expect(result.protobuf.protoFiles).toHaveLength(1); + expect(result.proxy.inherit).toBe(true); + }); + }); + }); + + describe('transformBrunoConfigBeforeSave - Cleanup optional fields', () => { + test('should remove disabled: false from proxy config', () => { + const config = { + name: 'Test Collection', + proxy: { + disabled: false, + inherit: false, + config: { + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + username: 'user', + password: 'pass' + }, + bypassProxy: '' + } + } + }; + + const result = transformBrunoConfigBeforeSave(config); + + expect(result.proxy.disabled).toBeUndefined(); + expect(result.proxy.inherit).toBe(false); + }); + + test('should keep disabled: true in proxy config', () => { + const config = { + name: 'Test Collection', + proxy: { + disabled: true, + inherit: false, + config: { + protocol: 'http', + hostname: '', + port: null, + auth: { + username: '', + password: '' + }, + bypassProxy: '' + } + } + }; + + const result = transformBrunoConfigBeforeSave(config); + + expect(result.proxy.disabled).toBe(true); + }); + + test('should remove auth.disabled: false from proxy config', () => { + const config = { + name: 'Test Collection', + proxy: { + inherit: false, + config: { + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + disabled: false, + username: 'user', + password: 'pass' + }, + bypassProxy: '' + } + } + }; + + const result = transformBrunoConfigBeforeSave(config); + + expect(result.proxy.config.auth.disabled).toBeUndefined(); + expect(result.proxy.config.auth.username).toBe('user'); + }); + + test('should keep auth.disabled: true in proxy config', () => { + const config = { + name: 'Test Collection', + proxy: { + inherit: false, + config: { + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + disabled: true, + username: 'user', + password: 'pass' + }, + bypassProxy: '' + } + } + }; + + const result = transformBrunoConfigBeforeSave(config); + + expect(result.proxy.config.auth.disabled).toBe(true); + }); + + test('should handle missing proxy config', () => { + const config = { + name: 'Test Collection' + }; + + const result = transformBrunoConfigBeforeSave(config); + + expect(result.proxy).toBeUndefined(); + }); + + test('should preserve protobuf config cleanup', () => { + const config = { + name: 'Test Collection', + protobuf: { + protoFiles: [{ path: 'test.proto', exists: true }], + importPaths: [{ path: 'imports/', exists: false }] + }, + proxy: { + disabled: false, + inherit: false, + config: { + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + disabled: false, + username: 'user', + password: 'pass' + }, + bypassProxy: '' + } + } + }; + + const result = transformBrunoConfigBeforeSave(config); + + // Protobuf exists fields should be removed + expect(result.protobuf.protoFiles[0].exists).toBeUndefined(); + expect(result.protobuf.importPaths[0].exists).toBeUndefined(); + + // Proxy optional fields should be removed + expect(result.proxy.disabled).toBeUndefined(); + expect(result.proxy.config.auth.disabled).toBeUndefined(); + }); + + test('should not modify config without optional fields', () => { + const config = { + name: 'Test Collection', + proxy: { + inherit: true, + config: { + protocol: 'http', + hostname: '', + port: null, + auth: { + username: '', + password: '' + }, + bypassProxy: '' + } + } + }; + + const result = transformBrunoConfigBeforeSave(config); + + expect(result.proxy).toEqual(config.proxy); + }); + }); + + describe('Round-trip transformation', () => { + test('should handle read -> save -> read cycle for old format', async () => { + const oldConfig = { + name: 'Test Collection', + proxy: { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + enabled: true, + username: 'user', + password: 'pass' + }, + bypassProxy: 'localhost' + } + }; + + // Read (migrate) + const afterRead = await transformBrunoConfigAfterRead(oldConfig, '/test/path'); + + // Save (cleanup) + const beforeSave = transformBrunoConfigBeforeSave(afterRead); + + // Read again (should not change) + const afterSecondRead = await transformBrunoConfigAfterRead(beforeSave, '/test/path'); + + expect(afterSecondRead.proxy).toEqual(beforeSave.proxy); + expect(afterSecondRead.proxy.inherit).toBe(false); + expect(afterSecondRead.proxy.disabled).toBeUndefined(); + expect(afterSecondRead.proxy.config.auth.disabled).toBeUndefined(); + }); + + test('should handle read -> save -> read cycle for new format', async () => { + const newConfig = { + name: 'Test Collection', + proxy: { + inherit: false, + config: { + protocol: 'https', + hostname: 'proxy.example.com', + port: 8443, + auth: { + username: 'user', + password: 'pass' + }, + bypassProxy: '*.local' + } + } + }; + + // Read (no migration) + const afterRead = await transformBrunoConfigAfterRead(newConfig, '/test/path'); + + // Save (cleanup) + const beforeSave = transformBrunoConfigBeforeSave(afterRead); + + // Read again (should not change) + const afterSecondRead = await transformBrunoConfigAfterRead(beforeSave, '/test/path'); + + expect(afterSecondRead.proxy).toEqual(newConfig.proxy); + }); + }); +}); diff --git a/packages/bruno-filestore/package.json b/packages/bruno-filestore/package.json index 7f738784f..64e243cc3 100644 --- a/packages/bruno-filestore/package.json +++ b/packages/bruno-filestore/package.json @@ -22,6 +22,7 @@ "devDependencies": { "@babel/preset-env": "^7.22.0", "@babel/preset-typescript": "^7.22.0", + "@opencollection/types": "~0.5.0", "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.0.1", diff --git a/packages/bruno-filestore/src/formats/yml/common/auth-oauth2.ts b/packages/bruno-filestore/src/formats/yml/common/auth-oauth2.ts index 46c1db056..fd005b47a 100644 --- a/packages/bruno-filestore/src/formats/yml/common/auth-oauth2.ts +++ b/packages/bruno-filestore/src/formats/yml/common/auth-oauth2.ts @@ -88,7 +88,8 @@ const buildPkce = (pkce?: boolean | null): OAuth2PKCE | undefined => { return undefined; } - return { enabled: Boolean(pkce) }; + // If pkce is false, set disabled: true; if true, return empty object (enabled by default) + return pkce ? {} : { disabled: true }; }; const buildTokenConfig = (oauth: BrunoOAuth2): OAuth2TokenConfig | undefined => { @@ -540,8 +541,9 @@ export const toBrunoOAuth2 = (oauth: AuthOAuth2 | null | undefined): BrunoOAuth2 if (brunoOAuth.grantType === 'authorization_code' && oauth.flow === 'authorization_code') { const authCodeFlow = oauth as OAuth2AuthorizationCodeFlow; - if (authCodeFlow.pkce?.enabled !== undefined) { - brunoOAuth.pkce = authCodeFlow.pkce.enabled; + if (authCodeFlow.pkce !== undefined) { + // If pkce.disabled is true, set pkce to false; otherwise set to true + brunoOAuth.pkce = !authCodeFlow.pkce.disabled; } } diff --git a/packages/bruno-filestore/src/formats/yml/parseCollection.ts b/packages/bruno-filestore/src/formats/yml/parseCollection.ts index c501d91ce..c192d1750 100644 --- a/packages/bruno-filestore/src/formats/yml/parseCollection.ts +++ b/packages/bruno-filestore/src/formats/yml/parseCollection.ts @@ -50,43 +50,53 @@ const parseCollection = (ymlString: string): ParsedCollection => { }; } - // proxy + // proxy - only support newer format + // Default: inherit from global preferences brunoConfig.proxy = { - enabled: false, - protocol: '', - hostname: '', - port: '', - auth: { - enabled: false, - username: '', - password: '' + inherit: true, + config: { + protocol: 'http', + hostname: '', + port: '', + auth: { + username: '', + password: '' + }, + bypassProxy: '' } }; - if (oc.config?.proxy) { - if (oc.config.proxy === 'inherit') { - brunoConfig.proxy.enabled = 'global'; - } else if (typeof oc.config.proxy === 'object') { + if (oc.config?.proxy && typeof oc.config.proxy === 'object') { + // Validate newer format: must have 'inherit' and 'config' properties + const proxyConfig = oc.config.proxy as any; + + if ('inherit' in proxyConfig && typeof proxyConfig.inherit === 'boolean' && proxyConfig.config && typeof proxyConfig.config === 'object') { + // Valid newer format brunoConfig.proxy = { - enabled: true, - protocol: oc.config.proxy.protocol || '', - hostname: oc.config.proxy.hostname || '', - port: oc.config.proxy.port || '', - auth: { - enabled: false, - username: '', - password: '' + inherit: proxyConfig.inherit, + config: { + protocol: proxyConfig.config.protocol || 'http', + hostname: proxyConfig.config.hostname || '', + port: proxyConfig.config.port || '', + auth: { + username: proxyConfig.config.auth?.username || '', + password: proxyConfig.config.auth?.password || '' + }, + bypassProxy: proxyConfig.config.bypassProxy || '' } }; - if (oc.config.proxy.auth && typeof oc.config.proxy.auth === 'object') { - brunoConfig.proxy.auth = { - enabled: true, - username: oc.config.proxy.auth.username || '', - password: oc.config.proxy.auth.password || '' - }; + // Handle optional disabled field + if (proxyConfig.disabled === true) { + brunoConfig.proxy.disabled = true; + } + + // Handle optional auth.disabled field + if (proxyConfig.config.auth?.disabled === true) { + brunoConfig.proxy.config.auth.disabled = true; } } + // If not in newer format, use default (inherit: true) } // client certificates diff --git a/packages/bruno-filestore/src/formats/yml/stringifyCollection.ts b/packages/bruno-filestore/src/formats/yml/stringifyCollection.ts index 76b8ce574..c3ae1cdaf 100644 --- a/packages/bruno-filestore/src/formats/yml/stringifyCollection.ts +++ b/packages/bruno-filestore/src/formats/yml/stringifyCollection.ts @@ -18,8 +18,15 @@ const hasCollectionConfig = (brunoConfig: any): boolean => { || brunoConfig.protobuf?.importPaths?.length > 0 ); - // proxy - const hasProxy = !!brunoConfig.proxy?.enabled; + // proxy - check if proxy is configured in newer format + // Valid newer format: has 'inherit' property and 'config' object + const isValidProxyFormat = brunoConfig.proxy + && typeof brunoConfig.proxy === 'object' + && 'inherit' in brunoConfig.proxy + && brunoConfig.proxy.config + && typeof brunoConfig.proxy.config === 'object'; + + const hasProxy = isValidProxyFormat; // client certificates const hasClientCertificates = brunoConfig.clientCertificates?.certs?.length > 0; @@ -52,7 +59,6 @@ const hasPresets = (brunoConfig: any): boolean => { }; const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => { - console.log('brunoConfig', brunoConfig); try { const oc: OpenCollection = {}; @@ -78,22 +84,38 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => { }; } - // proxy - if (brunoConfig.proxy?.enabled) { - if (brunoConfig.proxy.enabled === 'global') { - oc.config.proxy = 'inherit'; - } else { - oc.config.proxy = { - protocol: brunoConfig.proxy.protocol, - hostname: brunoConfig.proxy.hostname, - port: brunoConfig.proxy.port - }; + // proxy - only write newer format + // Validate that brunoConfig.proxy is in newer format before writing + const isValidProxyFormat = brunoConfig.proxy + && typeof brunoConfig.proxy === 'object' + && 'inherit' in brunoConfig.proxy + && brunoConfig.proxy.config + && typeof brunoConfig.proxy.config === 'object'; - if (brunoConfig.proxy.auth?.enabled) { - oc.config.proxy.auth = { - username: brunoConfig.proxy.auth.username, - password: brunoConfig.proxy.auth.password - }; + if (isValidProxyFormat) { + oc.config.proxy = { + inherit: brunoConfig.proxy.inherit, + config: { + protocol: brunoConfig.proxy.config.protocol || 'http', + hostname: brunoConfig.proxy.config.hostname || '', + port: brunoConfig.proxy.config.port || '', + auth: { + username: brunoConfig.proxy.config.auth?.username || '', + password: brunoConfig.proxy.config.auth?.password || '' + }, + bypassProxy: brunoConfig.proxy.config.bypassProxy || '' + } + }; + + // Add optional disabled field if true + if (brunoConfig.proxy.disabled === true) { + oc.config.proxy.disabled = true; + } + + // Add optional auth.disabled field if true + if (brunoConfig.proxy.config?.auth?.disabled === true) { + if (oc.config.proxy.config && oc.config.proxy.config.auth) { + oc.config.proxy.config.auth.disabled = true; } } }