fix(ui): correct “modified” indicator state across collection, folder, request, and presets/auth tabs (#3386) (#8027)

* fix: 3296 Folder-level No Auth inheritance is ignored; requests still use Collection Auth
This commit is contained in:
sharan-bruno
2026-06-08 16:57:18 +05:30
committed by GitHub
parent b9d8bdf2ec
commit 2d4d4e4037
41 changed files with 1182 additions and 356 deletions

View File

@@ -75,13 +75,13 @@ const AuthMode = ({ collection }) => {
return (
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
<div className="inline-flex items-center cursor-pointer auth-mode-selector" data-testid="auth-mode-selector">
<MenuDropdown
items={menuItems}
placement="bottom-end"
selectedItemId={authMode}
>
<div className="flex items-center justify-center auth-mode-label select-none">
<div className="flex items-center justify-center auth-mode-label select-none" data-testid="auth-mode-label">
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1" size={14} strokeWidth={2} />
</div>
</MenuDropdown>

View File

@@ -5,10 +5,11 @@ import { updateCollectionPresets } from 'providers/ReduxStore/slices/collections
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { get } from 'lodash';
import Button from 'ui/Button';
import { DEFAULT_PRESET_REQUEST_TYPE, PRESET_REQUEST_TYPES } from 'utils/common/constants';
const PresetsSettings = ({ collection }) => {
const dispatch = useDispatch();
const initialPresets = { requestType: 'http', requestUrl: '' };
const initialPresets = { requestType: DEFAULT_PRESET_REQUEST_TYPE, requestUrl: '' };
// Get presets from draft.brunoConfig if it exists, otherwise from brunoConfig
const currentPresets = collection.draft?.brunoConfig
@@ -47,12 +48,13 @@ const PresetsSettings = ({ collection }) => {
<div className="flex items-center">
<input
id="http"
data-testid="presets-request-type-http"
className="cursor-pointer"
type="radio"
name="requestType"
onChange={handleRequestTypeChange}
value="http"
checked={(currentPresets.requestType || 'http') === 'http'}
value={PRESET_REQUEST_TYPES.HTTP}
checked={(currentPresets.requestType || DEFAULT_PRESET_REQUEST_TYPE) === PRESET_REQUEST_TYPES.HTTP}
/>
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
HTTP
@@ -60,12 +62,13 @@ const PresetsSettings = ({ collection }) => {
<input
id="graphql"
data-testid="presets-request-type-graphql"
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={handleRequestTypeChange}
value="graphql"
checked={(currentPresets.requestType || 'http') === 'graphql'}
value={PRESET_REQUEST_TYPES.GRAPHQL}
checked={(currentPresets.requestType || DEFAULT_PRESET_REQUEST_TYPE) === PRESET_REQUEST_TYPES.GRAPHQL}
/>
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
GraphQL
@@ -73,12 +76,13 @@ const PresetsSettings = ({ collection }) => {
<input
id="grpc"
data-testid="presets-request-type-grpc"
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={handleRequestTypeChange}
value="grpc"
checked={(currentPresets.requestType || 'http') === 'grpc'}
value={PRESET_REQUEST_TYPES.GRPC}
checked={(currentPresets.requestType || DEFAULT_PRESET_REQUEST_TYPE) === PRESET_REQUEST_TYPES.GRPC}
/>
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
gRPC
@@ -86,12 +90,13 @@ const PresetsSettings = ({ collection }) => {
<input
id="ws"
data-testid="presets-request-type-ws"
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={handleRequestTypeChange}
value="ws"
checked={(currentPresets.requestType || 'http') === 'ws'}
value={PRESET_REQUEST_TYPES.WS}
checked={(currentPresets.requestType || DEFAULT_PRESET_REQUEST_TYPE) === PRESET_REQUEST_TYPES.WS}
/>
<label htmlFor="ws" className="ml-1 cursor-pointer select-none">
WebSocket
@@ -106,6 +111,7 @@ const PresetsSettings = ({ collection }) => {
<div className="flex items-center flex-grow input-container h-full">
<input
id="request-url"
data-testid="presets-request-url"
type="text"
name="requestUrl"
placeholder="Request URL"
@@ -123,7 +129,7 @@ const PresetsSettings = ({ collection }) => {
</div>
<div className="mt-6">
<Button type="button" size="sm" onClick={handleSave}>
<Button type="button" size="sm" data-testid="presets-save-btn" onClick={handleSave}>
Save
</Button>
</div>

View File

@@ -15,6 +15,7 @@ import StyledWrapper from './StyledWrapper';
import Vars from './Vars/index';
import StatusDot from 'components/StatusDot';
import Overview from './Overview/index';
import { DEFAULT_PRESET_REQUEST_TYPE } from 'utils/common/constants';
const CollectionSettings = ({ collection }) => {
const dispatch = useDispatch();
@@ -60,7 +61,7 @@ const CollectionSettings = ({ collection }) => {
? get(collection, 'draft.brunoConfig.protobuf', {})
: get(collection, 'brunoConfig.protobuf', {});
const presets = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.presets', {}) : get(collection, 'brunoConfig.presets', {});
const hasPresets = presets && presets.requestUrl !== '';
const hasPresets = presets && ((presets.requestType && presets.requestType !== DEFAULT_PRESET_REQUEST_TYPE) || (presets.requestUrl && presets.requestUrl !== ''));
const getTabPanel = (tab) => {
switch (tab) {

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import get from 'lodash/get';
import StyledWrapper from './StyledWrapper';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
@@ -18,8 +18,9 @@ import OAuth1 from 'components/RequestPane/Auth/OAuth1';
import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
import { humanizeRequestAuthMode, getTreePathFromCollectionToItem } from 'utils/collections/index';
import { humanizeRequestAuthMode } from 'utils/collections/index';
import Button from 'ui/Button';
import { getEffectiveAuthSource } from 'utils/auth';
const GrantTypeComponentMap = ({ collection, folder, updateFolderAuth }) => {
const dispatch = useDispatch();
@@ -52,41 +53,6 @@ const Auth = ({ collection, folder }) => {
let request = get(folderRoot, 'request', {});
const authMode = get(folderRoot, 'request.auth.mode');
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
auth: collectionAuth
};
// Get path from collection to current folder
const folderTreePath = getTreePathFromCollectionToItem(collection, folder);
// Check parent folders to find closest auth configuration
// Skip the last item which is the current folder
for (let i = 0; i < folderTreePath.length - 1; i++) {
const parentFolder = folderTreePath[i];
if (parentFolder.type === 'folder') {
const parentFolderRoot = parentFolder?.draft || parentFolder?.root;
const folderAuth = get(parentFolderRoot, 'request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {
effectiveSource = {
type: 'folder',
name: parentFolder.name,
auth: folderAuth
};
break;
}
}
}
return effectiveSource;
};
const handleSave = () => {
dispatch(saveFolderRoot(collection.uid, folder.uid));
};
@@ -98,6 +64,11 @@ const Auth = ({ collection, folder }) => {
});
};
const inheritedSource = useMemo(
() => (authMode === 'inherit' ? getEffectiveAuthSource(collection, folder) : null),
[authMode, folder, collection]
);
const getAuthView = () => {
switch (authMode) {
case 'basic': {
@@ -202,12 +173,11 @@ const Auth = ({ collection, folder }) => {
);
}
case 'inherit': {
const source = getEffectiveAuthSource();
return (
<>
<div className="flex flex-row w-full mt-2 gap-2">
<div>Auth inherited from {source.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
<div>Auth inherited from {inheritedSource.name}: </div>
<div className="inherit-mode-text" data-testid="inherited-auth-mode">{humanizeRequestAuthMode(inheritedSource.auth?.mode)}</div>
</div>
</>
);

View File

@@ -81,14 +81,15 @@ const AuthMode = ({ collection, folder }) => {
return (
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
<div className="inline-flex items-center cursor-pointer auth-mode-selector" data-testid="auth-mode-selector">
<MenuDropdown
items={menuItems}
placement="bottom-end"
selectedItemId={authMode}
showTickMark={true}
data-testid="auth-mode-dropdown"
>
<div className="flex items-center justify-center auth-mode-label select-none">
<div className="flex items-center justify-center auth-mode-label select-none" data-testid="auth-mode-label">
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
</MenuDropdown>

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import classnames from 'classnames';
import { updatedFolderSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
import { useDispatch } from 'react-redux';
@@ -10,7 +10,7 @@ import Vars from './Vars';
import Documentation from './Documentation';
import Auth from './Auth';
import StatusDot from 'components/StatusDot';
import get from 'lodash/get';
import { hasEffectiveAuth } from 'utils/auth';
const FolderSettings = ({ collection, folder }) => {
const dispatch = useDispatch();
@@ -31,8 +31,11 @@ const FolderSettings = ({ collection, folder }) => {
const responseVars = folderRoot?.request?.vars?.res || [];
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
const auth = get(folderRoot, 'request.auth.mode');
const hasAuth = auth && auth !== 'none';
const folderAuthMode = folder?.draft?.request?.auth?.mode ?? folder?.root?.request?.auth?.mode;
const hasAuth = useMemo(
() => hasEffectiveAuth(collection, folder),
[folder, folderAuthMode, collection]
);
const setTab = (tab) => {
dispatch(
@@ -95,7 +98,7 @@ const FolderSettings = ({ collection, folder }) => {
</div>
<div className={getTabClassname('auth')} role="tab" data-testid="folder-settings-tab-auth" onClick={() => setTab('auth')}>
Auth
{hasAuth && <StatusDot />}
{hasAuth && <StatusDot dataTestId="auth" />}
</div>
<div className={getTabClassname('docs')} role="tab" data-testid="folder-settings-tab-docs" onClick={() => setTab('docs')}>
Docs

View File

@@ -81,14 +81,15 @@ const AuthMode = ({ item, collection }) => {
return (
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
<div className="inline-flex items-center cursor-pointer auth-mode-selector" data-testid="auth-mode-selector">
<MenuDropdown
items={menuItems}
placement="bottom-end"
selectedItemId={authMode}
showTickMark={true}
data-testid="auth-mode-dropdown"
>
<div className="flex items-center justify-center auth-mode-label select-none">
<div className="flex items-center justify-center auth-mode-label select-none" data-testid="auth-mode-label">
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1" size={14} strokeWidth={2} />
</div>
</MenuDropdown>

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import get from 'lodash/get';
import AwsV4Auth from './AwsV4Auth';
import BearerAuth from './BearerAuth';
@@ -15,22 +15,11 @@ import ApiKeyAuth from './ApiKeyAuth';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAuthMode } from 'utils/collections';
import OAuth2 from './OAuth2/index';
import { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';
const getTreePathFromCollectionToItem = (collection, _item) => {
let path = [];
let item = findItemInCollection(collection, _item?.uid);
while (item) {
path.unshift(item);
item = findParentItemInCollection(collection, item?.uid);
}
return path;
};
import { getEffectiveAuthSource } from 'utils/auth';
const Auth = ({ item, collection }) => {
const dispatch = useDispatch();
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
// Create a request object to pass to the auth components
const request = item.draft
@@ -42,34 +31,10 @@ const Auth = ({ item, collection }) => {
return dispatch(saveRequest(item.uid, collection.uid));
};
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
auth: collectionAuth
};
// Check folders in reverse to find the closest auth configuration
for (let i of [...requestTreePath].reverse()) {
if (i.type === 'folder') {
const folderAuth = get(i, 'root.request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {
effectiveSource = {
type: 'folder',
name: i.name,
auth: folderAuth
};
break;
}
}
}
return effectiveSource;
};
const inheritedSource = useMemo(
() => (authMode === 'inherit' ? getEffectiveAuthSource(collection, item) : null),
[authMode, item, collection]
);
const getAuthView = () => {
switch (authMode) {
@@ -104,12 +69,11 @@ const Auth = ({ item, collection }) => {
return <ApiKeyAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
}
case 'inherit': {
const source = getEffectiveAuthSource();
return (
<>
<div className="flex flex-row w-full gap-2">
<div>Auth inherited from {source.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
<div>Auth inherited from {inheritedSource.name}: </div>
<div className="inherit-mode-text" data-testid="inherited-auth-mode">{humanizeRequestAuthMode(inheritedSource.auth?.mode)}</div>
</div>
</>
);

View File

@@ -24,10 +24,12 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
import Documentation from 'components/Documentation/index';
import useGraphqlSchema from '../GraphQLSchemaActions/useGraphqlSchema';
import { findEnvironmentInCollection } from 'utils/collections';
import { hasEffectiveAuth } from 'utils/auth';
import HeightBoundContainer from 'ui/HeightBoundContainer';
import Settings from 'components/RequestPane/Settings';
import ResponsiveTabs from 'ui/ResponsiveTabs';
import AuthMode from '../Auth/AuthMode/index';
import StatusDot from 'components/StatusDot';
const TAB_CONFIG = [
{ key: 'query', label: 'Query' },
@@ -172,7 +174,20 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
[dispatch, item.uid]
);
const allTabs = useMemo(() => TAB_CONFIG.map(({ key, label }) => ({ key, label })), []);
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
const hasAuth = useMemo(
() => hasEffectiveAuth(collection, item),
[item, itemAuthMode, collection]
);
const allTabs = useMemo(
() => TAB_CONFIG.map(({ key, label }) => ({
key,
label,
indicator: key === 'auth' && hasAuth ? <StatusDot dataTestId="auth" /> : null
})),
[hasAuth]
);
const handlePrettify = useCallback(() => {
if (queryEditorRef.current?.beautifyRequestBody) {

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import GrpcAuthMode from './GrpcAuthMode';
@@ -9,32 +9,32 @@ import OAuth2 from '../../Auth/OAuth2/index';
import WsseAuth from '../../Auth/WsseAuth';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAuthMode } from 'utils/collections';
import { getTreePathFromCollectionToItem } from 'utils/collections/index';
import { getEffectiveAuthSource } from 'utils/auth';
import { updateRequestAuthMode, updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
// List of auth modes supported by gRPC
// Note: Only header-based auth modes work with gRPC
// Complex auth modes like AWS Sig v4, Digest, and NTLM require axios interceptors
// and cannot be supported in gRPC requests as of now
const supportedGrpcAuthModes = ['basic', 'bearer', 'apikey', 'oauth2', 'wsse', 'none', 'inherit'];
import { AUTH_MODES_GRPC } from 'utils/common/constants';
const GrpcAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
const request = item.draft
? get(item, 'draft.request', {})
: get(item, 'request', {});
const inheritedSource = useMemo(
() => (authMode === 'inherit' ? getEffectiveAuthSource(collection, item) : null),
[authMode, item, collection]
);
const save = () => {
return saveRequest(item.uid, collection.uid);
};
// Reset to 'none' if current auth mode is not supported by gRPC
useEffect(() => {
if (authMode && !supportedGrpcAuthModes.includes(authMode)) {
if (authMode && !AUTH_MODES_GRPC.includes(authMode)) {
dispatch(
updateRequestAuthMode({
itemUid: item.uid,
@@ -45,35 +45,6 @@ const GrpcAuth = ({ item, collection }) => {
}
}, [authMode, collection.uid, dispatch, item.uid]);
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
auth: collectionAuth
};
// Check folders in reverse to find the closest auth configuration
for (let i of [...requestTreePath].reverse()) {
if (i.type === 'folder') {
const folderAuth = get(i, 'root.request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
effectiveSource = {
type: 'folder',
name: i.name,
auth: folderAuth
};
break;
}
}
}
return effectiveSource;
};
const getAuthView = () => {
switch (authMode) {
case 'none': {
@@ -95,15 +66,13 @@ const GrpcAuth = ({ item, collection }) => {
return <WsseAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;
}
case 'inherit': {
const source = getEffectiveAuthSource();
// Only show inherited auth if it's one of the supported types
if (source && supportedGrpcAuthModes.includes(source.auth?.mode)) {
if (inheritedSource && AUTH_MODES_GRPC.includes(inheritedSource.auth?.mode)) {
return (
<>
<div className="flex flex-row w-full gap-2">
<div>Auth inherited from {source.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
<div>Auth inherited from {inheritedSource.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(inheritedSource.auth?.mode)}</div>
</div>
</>
);

View File

@@ -12,6 +12,8 @@ import Documentation from 'components/Documentation/index';
import { getPropertyFromDraftOrRequest } from 'utils/collections/index';
import ResponsiveTabs from 'ui/ResponsiveTabs';
import StyledWrapper from './StyledWrapper';
import { hasEffectiveAuth } from 'utils/auth';
import { AUTH_MODES_GRPC } from 'utils/common/constants';
const GrpcRequestPane = ({ item, collection, handleRun }) => {
const dispatch = useDispatch();
@@ -53,8 +55,11 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
const body = getPropertyFromDraftOrRequest(item, 'request.body');
const headers = getPropertyFromDraftOrRequest(item, 'request.headers');
const docs = getPropertyFromDraftOrRequest(item, 'request.docs');
const auth = getPropertyFromDraftOrRequest(item, 'request.auth');
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
const hasAuth = useMemo(
() => hasEffectiveAuth(collection, item, AUTH_MODES_GRPC),
[item, itemAuthMode, collection]
);
const activeHeadersLength = headers.filter((header) => header.enabled).length;
const grpcMessagesCount = body?.grpc?.length || 0;
@@ -88,7 +93,7 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
{
key: 'auth',
label: 'Auth',
indicator: auth?.mode && auth.mode !== 'none' ? <StatusDot type="default" /> : null
indicator: hasAuth ? <StatusDot type="default" dataTestId="auth" /> : null
},
{
key: 'docs',
@@ -96,7 +101,7 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
indicator: docs && docs.length > 0 ? <StatusDot type="default" /> : null
}
];
}, [grpcMessagesCount, isClientStreaming, activeHeadersLength, auth?.mode, docs]);
}, [grpcMessagesCount, isClientStreaming, activeHeadersLength, hasAuth, docs]);
// Initialize tab to 'body' if no tab is currently set
useEffect(() => {

View File

@@ -18,6 +18,7 @@ import StatusDot from 'components/StatusDot';
import ResponsiveTabs from 'ui/ResponsiveTabs';
import HeightBoundContainer from 'ui/HeightBoundContainer';
import AuthMode from '../Auth/AuthMode/index';
import { hasEffectiveAuth } from 'utils/auth';
const TAB_CONFIG = [
{ key: 'params', label: 'Params' },
@@ -54,7 +55,6 @@ const HttpRequestPane = ({ item, collection }) => {
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const requestPaneTab = focusedTab?.requestPaneTab;
const getProperty = useCallback(
(key) => (item.draft ? get(item, `draft.${key}`, []) : get(item, key, [])),
[item.draft, item]
@@ -86,6 +86,12 @@ const HttpRequestPane = ({ item, collection }) => {
[dispatch, item.uid]
);
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
const hasAuth = useMemo(
() => hasEffectiveAuth(collection, item),
[item, itemAuthMode, collection]
);
const indicators = useMemo(() => {
const hasScriptError = item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage;
const hasTestError = item.testScriptErrorMessage;
@@ -94,7 +100,7 @@ const HttpRequestPane = ({ item, collection }) => {
params: activeCounts.params > 0 ? <sup className="font-medium">{activeCounts.params}</sup> : null,
body: body.mode !== 'none' ? <StatusDot /> : null,
headers: activeCounts.headers > 0 ? <sup className="font-medium">{activeCounts.headers}</sup> : null,
auth: auth.mode !== 'none' ? <StatusDot /> : null,
auth: hasAuth ? <StatusDot dataTestId="auth" /> : null,
vars: activeCounts.vars > 0 ? <sup className="font-medium">{activeCounts.vars}</sup> : null,
script: (script.req || script.res) ? (hasScriptError ? <StatusDot type="error" /> : <StatusDot />) : null,
assert: activeCounts.assertions > 0 ? <sup className="font-medium">{activeCounts.assertions}</sup> : null,
@@ -102,7 +108,7 @@ const HttpRequestPane = ({ item, collection }) => {
docs: docs?.length > 0 ? <StatusDot /> : null,
settings: tags?.length > 0 ? <StatusDot /> : null
};
}, [activeCounts, body.mode, auth.mode, script, item.preRequestScriptErrorMessage, item.postResponseScriptErrorMessage, item.testScriptErrorMessage, tests, docs, tags]);
}, [activeCounts, body.mode, hasAuth, script, item.preRequestScriptErrorMessage, item.postResponseScriptErrorMessage, item.testScriptErrorMessage, tests, docs, tags]);
const allTabs = useMemo(
() => TAB_CONFIG.map(({ key, label }) => ({ key, label, indicator: indicators[key] })),

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import BearerAuth from '../../Auth/BearerAuth';
@@ -6,16 +6,15 @@ import BasicAuth from '../../Auth/BasicAuth';
import ApiKeyAuth from '../../Auth/ApiKeyAuth';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAuthMode } from 'utils/collections';
import { getTreePathFromCollectionToItem } from 'utils/collections/index';
import { getEffectiveAuthSource } from 'utils/auth';
import { updateRequestAuthMode, updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
const supportedAuthModes = ['basic', 'bearer', 'apikey', 'oauth2', 'none', 'inherit'];
import { AUTH_MODES_WS } from 'utils/common/constants';
const WSAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
const request = item.draft
? get(item, 'draft.request', {})
@@ -25,9 +24,14 @@ const WSAuth = ({ item, collection }) => {
return saveRequest(item.uid, collection.uid);
};
const inheritedSource = useMemo(
() => (authMode === 'inherit' ? getEffectiveAuthSource(collection, item) : null),
[authMode, item, collection]
);
// Reset to 'none' if current auth mode is not supported
useEffect(() => {
if (authMode && !supportedAuthModes.includes(authMode)) {
if (authMode && !AUTH_MODES_WS.includes(authMode)) {
dispatch(updateRequestAuthMode({
itemUid: item.uid,
collectionUid: collection.uid,
@@ -36,35 +40,6 @@ const WSAuth = ({ item, collection }) => {
}
}, [authMode, collection.uid, dispatch, item.uid]);
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
auth: collectionAuth
};
// Check folders in reverse to find the closest auth configuration
for (let i of [...requestTreePath].reverse()) {
if (i.type === 'folder') {
const folderAuth = get(i, 'root.request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
effectiveSource = {
type: 'folder',
name: i.name,
auth: folderAuth
};
break;
}
}
}
return effectiveSource;
};
const getAuthView = () => {
switch (authMode) {
case 'none': {
@@ -91,26 +66,24 @@ const WSAuth = ({ item, collection }) => {
);
}
case 'inherit': {
const source = getEffectiveAuthSource();
// Check if inherited auth is OAuth1/OAuth2 - not supported for WebSockets
if (source?.auth?.mode === 'oauth1' || source?.auth?.mode === 'oauth2') {
if (inheritedSource?.auth?.mode === 'oauth1' || inheritedSource?.auth?.mode === 'oauth2') {
return (
<>
<div className="flex flex-row w-full mt-2 gap-2">
{source.auth.mode === 'oauth1' ? 'OAuth 1.0' : 'OAuth 2'} not <strong>yet</strong> supported by WebSockets. Using no auth instead.
{inheritedSource.auth.mode === 'oauth1' ? 'OAuth 1.0' : 'OAuth 2'} not <strong>yet</strong> supported by WebSockets. Using no auth instead.
</div>
</>
);
}
// Only show inherited auth if it's one of the supported types
if (source && supportedAuthModes.includes(source.auth?.mode)) {
if (inheritedSource && AUTH_MODES_WS.includes(inheritedSource.auth?.mode)) {
return (
<>
<div className="flex flex-row w-full gap-2">
<div> Auth inherited from {source.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
<div> Auth inherited from {inheritedSource.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(inheritedSource.auth?.mode)}</div>
</div>
</>
);

View File

@@ -20,6 +20,8 @@ import StyledWrapper from './StyledWrapper';
import WSAuth from './WSAuth';
import WSAuthMode from './WSAuth/WSAuthMode';
import WSSettingsPane from '../WSSettingsPane/index';
import { hasEffectiveAuth } from 'utils/auth';
import { AUTH_MODES_WS } from 'utils/common/constants';
const WSRequestPane = ({ item, collection, handleRun }) => {
const dispatch = useDispatch();
@@ -102,8 +104,11 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
const headers = getPropertyFromDraftOrRequest(item, 'request.headers');
const docs = getPropertyFromDraftOrRequest(item, 'request.docs');
const auth = getPropertyFromDraftOrRequest(item, 'request.auth');
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
const hasAuth = useMemo(
() => hasEffectiveAuth(collection, item, AUTH_MODES_WS),
[item, itemAuthMode, collection]
);
const activeHeadersLength = headers.filter((header) => header.enabled).length;
const allTabs = useMemo(() => {
@@ -121,7 +126,7 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
{
key: 'auth',
label: 'Auth',
indicator: auth.mode !== 'none' ? <StatusDot type="default" /> : null
indicator: hasAuth ? <StatusDot type="default" dataTestId="auth" /> : null
},
{
key: 'settings',
@@ -134,7 +139,7 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
indicator: docs && docs.length > 0 ? <StatusDot type="default" /> : null
}
];
}, [activeHeadersLength, auth.mode, docs]);
}, [activeHeadersLength, hasAuth, docs]);
const tabPanel = useMemo(() => {
switch (requestPaneTab) {

View File

@@ -53,7 +53,12 @@ const Timeline = ({ collection, item }) => {
useTrackScroll({ ref: wrapperRef, selector: null, onChange: setScroll, initialValue: scroll });
const [activeFilter, setActiveFilter] = useState('all');
const authSource = getEffectiveAuthSource(collection, item);
// Get the effective auth source if auth mode is inherit
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
const authSource = useMemo(
() => getEffectiveAuthSource(collection, item),
[item, itemAuthMode, collection]
);
const isGrpcRequest = item.type === 'grpc-request' || item.type === 'ws-request';
const entries = useMemo(

View File

@@ -1,11 +1,12 @@
import React from 'react';
import DotIcon from 'components/Icons/Dot';
const StatusDot = ({ type = 'default' }) => (
const StatusDot = ({ type = 'default', dataTestId = null }) => (
<sup
className={`ml-[.125rem] opacity-80 font-medium ${
type === 'error' ? 'text-red-500' : ''
}`}
data-testid={dataTestId ? `status-dot-${dataTestId}` : 'status-dot'}
>
<DotIcon width="10" />
</sup>

View File

@@ -2,6 +2,7 @@ import { get } from 'lodash';
import {
getTreePathFromCollectionToItem
} from 'utils/collections/index';
import { AUTH_MODES } from 'utils/common/constants';
// Resolve inherited auth by traversing up the folder hierarchy
export const resolveInheritedAuth = (item, collection) => {
@@ -25,8 +26,9 @@ export const resolveInheritedAuth = (item, collection) => {
const collectionAuth = get(collectionRoot, 'request.auth', { mode: 'none' });
let effectiveAuth = collectionAuth;
// Check folders in reverse to find the closest auth configuration
for (let i of [...requestTreePath].reverse()) {
// Walk ancestor folders from deepest up; pick the first one with a concrete auth mode (skip 'none'/'inherit').
for (let idx = requestTreePath.length - 1; idx >= 0; idx--) {
const i = requestTreePath[idx];
if (i.type === 'folder') {
const folderAuth = i?.draft ? get(i, 'draft.request.auth') : get(i, 'root.request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
@@ -41,3 +43,51 @@ export const resolveInheritedAuth = (item, collection) => {
auth: effectiveAuth
};
};
export const getEffectiveAuthSource = (collection, item) => {
const authMode = item?.draft
? get(item, 'draft.request.auth.mode')
: (get(item, 'request.auth.mode') ?? get(item, 'root.request.auth.mode'));
if (authMode !== AUTH_MODES.INHERIT) return null;
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
auth: collectionAuth
};
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
for (let idx = requestTreePath.length - 1; idx >= 0; idx--) {
const i = requestTreePath[idx];
if (i?.uid === item?.uid) continue;
if (i?.type !== 'folder') continue;
const folderAuth = i?.draft ? get(i, 'draft.request.auth') : get(i, 'root.request.auth');
if (!folderAuth || !folderAuth.mode) continue;
if (folderAuth.mode === AUTH_MODES.INHERIT) continue;
effectiveSource = {
type: 'folder',
name: i.name,
auth: folderAuth
};
break;
}
return effectiveSource;
};
// Returns true when an item actually has auth applied — resolves `inherit` up
// the chain, then checks that the effective mode is set, not 'none', and (if a
// supportedModes list is passed) is one the protocol can apply.
export const hasEffectiveAuth = (collection, item, supportedModes) => {
const auth = item?.draft
? get(item, 'draft.request.auth')
: (get(item, 'request.auth') ?? get(item, 'root.request.auth'));
const mode = auth?.mode === AUTH_MODES.INHERIT
? getEffectiveAuthSource(collection, item)?.auth?.mode
: auth?.mode;
if (!mode || mode === AUTH_MODES.NONE) return false;
if (supportedModes && !supportedModes.includes(mode)) return false;
return true;
};

View File

@@ -1,13 +1,21 @@
import { resolveInheritedAuth } from './index';
import { getEffectiveAuthSource, resolveInheritedAuth } from './index';
jest.mock('utils/collections/index', () => ({
// General path finder: walks the collection.items tree until it finds the
// item with the matching uid and returns the full path to it.
getTreePathFromCollectionToItem: (collection, item) => {
const itemUid = item.uid;
if (itemUid === 'r1') {
return [collection.items[0], collection.items[0].items[0]];
}
return [];
const findPath = (items, targetUid, path = []) => {
for (const i of items || []) {
const next = [...path, i];
if (i.uid === targetUid) return next;
if (i.items) {
const found = findPath(i.items, targetUid, next);
if (found) return found;
}
}
return null;
};
return findPath(collection.items, item?.uid) || [];
}
}));
@@ -17,7 +25,7 @@ const buildCollection = () => {
uid: 'c1',
root: {
request: {
auth: { mode: 'bearer', bearer: { token: 'COLLECTION' } }
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
}
},
items: [
@@ -64,7 +72,7 @@ describe('auth-utils.resolveInheritedAuth', () => {
const resolved = resolveInheritedAuth(item, collection);
expect(resolved.auth.mode).toBe('bearer');
expect(resolved.auth.bearer.token).toBe('COLLECTION');
expect(resolved.auth.bearer.token).toBe('COLLECTION_LEVEL_TOKEN');
});
it('should return original request when mode is not inherit', () => {
@@ -77,3 +85,211 @@ describe('auth-utils.resolveInheritedAuth', () => {
expect(resolved.auth.basic.username).toBe('override');
});
});
describe('auth-utils.getEffectiveAuthSource', () => {
it('returns null when the request mode is not inherit', () => {
const collection = buildCollection();
const item = collection.items[0].items[0]; // r1
item.request.auth = { mode: 'bearer', bearer: { token: 'MOCK_REQUEST_OWN_TOKEN_STRING' } };
expect(getEffectiveAuthSource(collection, item)).toBeNull();
});
it('returns null when the request has no auth configured', () => {
const collection = buildCollection();
const item = collection.items[0].items[0];
item.request.auth = undefined;
expect(getEffectiveAuthSource(collection, item)).toBeNull();
});
it('returns the nearest configured folder when request inherits and folder has auth', () => {
const collection = buildCollection();
const item = collection.items[0].items[0]; // r1, mode 'inherit'
const source = getEffectiveAuthSource(collection, item);
expect(source).toEqual({
type: 'folder',
name: 'Folder',
auth: { mode: 'basic', basic: { username: 'user', password: 'pass' } }
});
});
it('falls back to the collection when no ancestor folder has configured auth', () => {
const collection = buildCollection();
// make the folder also inherit so the walk falls through to the collection
collection.items[0].root.request.auth = { mode: 'inherit' };
const item = collection.items[0].items[0];
const source = getEffectiveAuthSource(collection, item);
expect(source).toEqual({
type: 'collection',
name: 'Collection',
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
});
});
it('skips the item itself when the item is a folder in inherit mode', () => {
// Build a parent → child folder chain; child is the item under test.
const collection = {
uid: 'c1',
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
items: [
{
uid: 'parent',
type: 'folder',
name: 'Parent',
root: { request: { auth: { mode: 'basic', basic: { username: 'p', password: 'p' } } } },
items: [
{
uid: 'child',
type: 'folder',
name: 'Child',
root: { request: { auth: { mode: 'inherit' } } },
items: []
}
]
}
]
};
const child = collection.items[0].items[0];
const source = getEffectiveAuthSource(collection, child);
expect(source).toEqual({
type: 'folder',
name: 'Parent',
auth: { mode: 'basic', basic: { username: 'p', password: 'p' } }
});
});
it('prefers the draft mode when item.draft exists', () => {
const collection = buildCollection();
const item = collection.items[0].items[0];
item.request.auth = { mode: 'bearer' }; // saved is not inherit
item.draft = { request: { auth: { mode: 'inherit' } } }; // draft is inherit
const source = getEffectiveAuthSource(collection, item);
expect(source).toEqual({
type: 'folder',
name: 'Folder',
auth: { mode: 'basic', basic: { username: 'user', password: 'pass' } }
});
});
it('resolves correctly when both draft and saved auth are inherit on a folder whose parent is also a folder', () => {
const collection = {
uid: 'c1',
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
items: [
{
uid: 'parent',
type: 'folder',
name: 'Parent',
root: { request: { auth: { mode: 'basic', basic: { username: 'p', password: 'p' } } } },
items: [
{
uid: 'child',
type: 'folder',
name: 'Child',
root: { request: { auth: { mode: 'inherit' } } }, // saved: inherit
draft: { request: { auth: { mode: 'inherit' } } }, // draft: inherit
items: []
}
]
}
]
};
const child = collection.items[0].items[0];
const source = getEffectiveAuthSource(collection, child);
expect(source).toEqual({
type: 'folder',
name: 'Parent',
auth: { mode: 'basic', basic: { username: 'p', password: 'p' } }
});
});
it('handles a folder item without draft using its root.request.auth.mode', () => {
// The folder's mode is read from root.request.auth.mode when no draft exists.
const collection = {
uid: 'c1',
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
items: [
{
uid: 'folder-inherit',
type: 'folder',
name: 'FolderInherit',
root: { request: { auth: { mode: 'inherit' } } },
items: []
}
]
};
const folder = collection.items[0];
const source = getEffectiveAuthSource(collection, folder);
expect(source).toEqual({
type: 'collection',
name: 'Collection',
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
});
});
it('handles a folder item with draft by reading its draft.request.auth.mode (not the saved root mode)', () => {
// Saved mode is 'basic' (would return null since not inherit), but draft is 'inherit'
// so the walk should run and resolve to the collection.
const collection = {
uid: 'c1',
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
items: [
{
uid: 'folder-draft-inherit',
type: 'folder',
name: 'FolderDraftInherit',
root: { request: { auth: { mode: 'basic', basic: { username: 'saved', password: 'saved' } } } },
draft: { request: { auth: { mode: 'inherit' } } },
items: []
}
]
};
const folder = collection.items[0];
const source = getEffectiveAuthSource(collection, folder);
expect(source).toEqual({
type: 'collection',
name: 'Collection',
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
});
});
it('skips ancestor folders whose auth.mode is itself "inherit"', () => {
// Parent folder also inherits — walk should continue past it to collection.
const collection = {
uid: 'c1',
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
items: [
{
uid: 'parent',
type: 'folder',
name: 'Parent',
root: { request: { auth: { mode: 'inherit' } } },
items: [
{
uid: 'r1',
type: 'request',
name: 'Request',
request: { auth: { mode: 'inherit' } }
}
]
}
]
};
const item = collection.items[0].items[0];
const source = getEffectiveAuthSource(collection, item);
expect(source).toEqual({
type: 'collection',
name: 'Collection',
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
});
});
});

View File

@@ -1,3 +1,47 @@
export const REQUEST_TYPES = ['http-request', 'graphql-request', 'grpc-request', 'ws-request'];
export const DEFAULT_COLLECTION_FORMAT = 'yml';
export const PRESET_REQUEST_TYPES = {
HTTP: 'http',
GRAPHQL: 'graphql',
GRPC: 'grpc',
WS: 'ws'
};
export const DEFAULT_PRESET_REQUEST_TYPE = PRESET_REQUEST_TYPES.HTTP;
export const AUTH_MODES = {
AWSV4: 'awsv4',
BASIC: 'basic',
BEARER: 'bearer',
DIGEST: 'digest',
NTLM: 'ntlm',
OAUTH1: 'oauth1',
OAUTH2: 'oauth2',
WSSE: 'wsse',
APIKEY: 'apikey',
NONE: 'none',
INHERIT: 'inherit'
};
// Auth modes supported by WS protocol.
export const AUTH_MODES_WS = [
AUTH_MODES.BASIC,
AUTH_MODES.BEARER,
AUTH_MODES.APIKEY,
AUTH_MODES.OAUTH2,
AUTH_MODES.NONE,
AUTH_MODES.INHERIT
];
// Auth modes supported by GRPC protocol
export const AUTH_MODES_GRPC = [
AUTH_MODES.BASIC,
AUTH_MODES.BEARER,
AUTH_MODES.APIKEY,
AUTH_MODES.OAUTH2,
AUTH_MODES.WSSE,
AUTH_MODES.NONE,
AUTH_MODES.INHERIT
];

View File

@@ -2,7 +2,7 @@ import { toOpenCollectionAuth, toOpenCollectionHeaders, toOpenCollectionScripts,
import { toOpenCollectionEnvironments } from "./environment";
import { toOpenCollectionFolder } from "./folder";
import { toOpenCollectionItems } from "./items";
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, ClientCertificate, CollectionConfig, OpenCollection, PemCertificate, Pkcs12Certificate, Protobuf } from "./types";
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, BrunoPresets, ClientCertificate, CollectionConfig, OpenCollection, PemCertificate, Pkcs12Certificate, Protobuf } from "./types";
const toOpenCollectionConfig = (brunoConfig: BrunoConfig | undefined): CollectionConfig | undefined => {
if (!brunoConfig) {
@@ -147,10 +147,7 @@ export const brunoToOpenCollection = (collection: BrunoCollection): OpenCollecti
const brunoExtension: {
ignore?: string[];
presets?: {
requestType?: string;
requestUrl?: string;
};
presets?: BrunoPresets;
} = {};
if ((collection.brunoConfig as BrunoConfig)?.ignore?.length) {

View File

@@ -1,5 +1,5 @@
import { OpenCollection } from "@opencollection/types";
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, PemCertificate, Pkcs12Certificate } from "./types";
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, BrunoPresets, PemCertificate, Pkcs12Certificate } from "./types";
import { fromOpenCollectionAuth, fromOpenCollectionHeaders, fromOpenCollectionScripts, fromOpenCollectionVariables } from "./common";
import { uuid } from "../common";
import { fromOpenCollectionItems } from "./items";
@@ -9,10 +9,7 @@ import { fromOpenCollectionEnvironments } from "./environment";
const fromOpenCollectionConfig = (oc: OpenCollection): BrunoConfig => {
const brunoExtension = oc.extensions?.bruno as {
ignore?: string[];
presets?: {
requestType?: string;
requestUrl?: string;
};
presets?: BrunoPresets;
} | undefined;
const ignoreList = brunoExtension && Array.isArray(brunoExtension.ignore)

View File

@@ -168,15 +168,17 @@ export type {
WebSocketMessage as BrunoWsMessage
} from '@usebruno/schema-types/requests/websocket';
export interface BrunoPresets {
requestType?: string;
requestUrl?: string;
}
export interface BrunoConfig {
version?: string;
name?: string;
type?: string;
ignore?: string[];
presets?: {
requestType?: string;
requestUrl?: string;
};
presets?: BrunoPresets;
protobuf?: {
protoFiles?: { path: string }[];
importPaths?: { path: string; enabled?: boolean }[];

View File

@@ -809,7 +809,7 @@ const mergeAuth = (collection, request, requestTreePath) => {
const folderRoot = i?.draft || i?.root;
const folderAuth = get(folderRoot, 'request.auth');
// Only consider folders that have a valid auth mode
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {
effectiveAuth = folderAuth;
lastFolderWithAuth = i;
}

View File

@@ -7,6 +7,7 @@ import { toBrunoVariables } from './common/variables';
import { toBrunoPostResponseVariables } from './common/actions';
import { toBrunoScripts } from './common/scripts';
import { ensureString } from '../../utils';
import type { BrunoPresetsExtension } from '../../types';
interface ParsedCollection {
collectionRoot: FolderRoot;
@@ -32,11 +33,11 @@ const parseCollection = (ymlString: string): ParsedCollection => {
// presets
if (brunoExtension?.presets) {
const presets = brunoExtension.presets as any;
const presets = brunoExtension.presets as BrunoPresetsExtension;
if (presets.request) {
brunoConfig.presets = {
requestType: presets.request.type || [],
requestUrl: presets.request.url || []
requestType: presets.request.type || '',
requestUrl: presets.request.url || ''
};
}
}

View File

@@ -19,15 +19,9 @@ const parseFolder = (ymlString: string): FolderRoot => {
name: ensureString(info?.name, 'Untitled Folder'),
seq: info?.seq || 1
},
request: null,
docs: null
};
// request defaults
if (ocFolder.request) {
folderRoot.request = {
request: {
headers: [],
auth: null,
auth: toBrunoAuth(ocFolder.request?.auth),
script: {
req: null,
res: null
@@ -37,40 +31,39 @@ const parseFolder = (ymlString: string): FolderRoot => {
res: []
},
tests: null
};
},
docs: null
};
if (ocFolder.request) {
const folderRequest = folderRoot.request!;
// headers
const headers = toBrunoHttpHeaders(ocFolder.request.headers);
if (headers) {
folderRoot.request.headers = headers;
}
// auth
const auth = toBrunoAuth(ocFolder.request.auth);
if (auth) {
folderRoot.request.auth = auth;
folderRequest.headers = headers;
}
// variables
const variables = toBrunoVariables(ocFolder.request.variables);
const postResponseVars = toBrunoPostResponseVariables((ocFolder.request as any).actions);
folderRoot.request.vars = {
folderRequest.vars = {
req: variables.req,
res: postResponseVars
};
// scripts
const scripts = toBrunoScripts(ocFolder.request.scripts);
if (scripts?.script && folderRoot.request.script) {
if (scripts?.script && folderRequest.script) {
if (scripts.script.req) {
folderRoot.request.script.req = scripts.script.req;
folderRequest.script.req = scripts.script.req;
}
if (scripts.script.res) {
folderRoot.request.script.res = scripts.script.res;
folderRequest.script.res = scripts.script.res;
}
}
if (scripts?.tests) {
folderRoot.request.tests = scripts.tests;
folderRequest.tests = scripts.tests;
}
}

View File

@@ -0,0 +1,7 @@
opencollection: 1.0.0
info:
name: My Collection
extensions:
bruno:
presets:
request: {}

View File

@@ -0,0 +1,3 @@
opencollection: 1.0.0
info:
name: My Collection

View File

@@ -0,0 +1,6 @@
opencollection: 1.0.0
info:
name: My Collection
extensions:
bruno:
presets: {}

View File

@@ -0,0 +1,24 @@
opencollection: 1.0.0
info:
name: md8
config:
proxy:
inherit: true
config:
protocol: http
hostname: ""
port: ""
auth:
username: ""
password: ""
bypassProxy: ""
bundled: false
extensions:
bruno:
ignore:
- node_modules
- .git
presets:
request:
type: graphql

View File

@@ -0,0 +1,25 @@
opencollection: 1.0.0
info:
name: md8
config:
proxy:
inherit: true
config:
protocol: http
hostname: ""
port: ""
auth:
username: ""
password: ""
bypassProxy: ""
bundled: false
extensions:
bruno:
ignore:
- node_modules
- .git
presets:
request:
type: http
url: https://example.com/graphql

View File

@@ -0,0 +1,9 @@
opencollection: 1.0.0
info:
name: My Collection
extensions:
bruno:
presets:
request:
type: graphql
url: https://example.com/graphql

View File

@@ -0,0 +1,63 @@
import fs from 'node:fs';
import path from 'node:path';
import parseCollection from '../parseCollection';
const loadFixture = (name) =>
fs.readFileSync(path.join(__dirname, 'fixtures', 'presets', `${name}.yml`), 'utf8');
describe('yml parseCollection - presets', () => {
it('parses presets when request.type and request.url are present', () => {
const { brunoConfig } = parseCollection(loadFixture('with-type-and-url'));
expect(brunoConfig.presets).toEqual({
requestType: 'graphql',
requestUrl: 'https://example.com/graphql'
});
});
it('defaults requestType and requestUrl to empty strings (not arrays) when request fields are missing', () => {
const { brunoConfig } = parseCollection(loadFixture('empty-request'));
expect(brunoConfig.presets).toEqual({
requestType: '',
requestUrl: ''
});
expect(Array.isArray(brunoConfig.presets.requestType)).toBe(false);
expect(Array.isArray(brunoConfig.presets.requestUrl)).toBe(false);
});
it('does not set presets when the extension has no presets block', () => {
const { brunoConfig } = parseCollection(loadFixture('no-presets'));
expect(brunoConfig.presets).toBeUndefined();
});
it('does not set presets when presets exists but request key is absent', () => {
const { brunoConfig } = parseCollection(loadFixture('no-request-key'));
expect(brunoConfig.presets).toBeUndefined();
});
it('parses a realistic collection with only request.type set (no url) — defaults url to empty string', () => {
const { brunoConfig } = parseCollection(loadFixture('type-only-realistic'));
expect(brunoConfig.presets).toEqual({
requestType: 'graphql',
requestUrl: ''
});
expect(Array.isArray(brunoConfig.presets.requestUrl)).toBe(false);
expect(brunoConfig.ignore).toEqual(['node_modules', '.git']);
});
it('parses a realistic collection with request.type http and request.url set', () => {
const { brunoConfig } = parseCollection(loadFixture('url-only-realistic'));
expect(brunoConfig.presets).toEqual({
requestType: 'http',
requestUrl: 'https://example.com/graphql'
});
expect(Array.isArray(brunoConfig.presets.requestType)).toBe(false);
expect(Array.isArray(brunoConfig.presets.requestUrl)).toBe(false);
expect(brunoConfig.ignore).toEqual(['node_modules', '.git']);
});
});

View File

@@ -20,3 +20,10 @@ export interface WorkerTask {
export interface Lane {
maxSize: number;
}
export interface BrunoPresetsExtension {
request?: {
type?: string;
url?: string;
};
}