mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 14:35:03 +00:00
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:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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' } }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user