mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-04 01:48:33 +00:00
Compare commits
15 Commits
oauth2_add
...
v2.10.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d77d05b34 | ||
|
|
518b1ed441 | ||
|
|
c1aa682c03 | ||
|
|
01275acc89 | ||
|
|
c8f223a000 | ||
|
|
4202b48edd | ||
|
|
69891c0bc7 | ||
|
|
76729519c6 | ||
|
|
22a77b90f9 | ||
|
|
48934ef74a | ||
|
|
9c16ebcda3 | ||
|
|
2ed51bb984 | ||
|
|
aec9ee6265 | ||
|
|
04d1e50f98 | ||
|
|
e71ee3eff5 |
@@ -69,6 +69,7 @@ npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
npm run build:bruno-filestore
|
||||
|
||||
# bundle js sandbox libraries
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
|
||||
154
eslint.config.js
154
eslint.config.js
@@ -5,7 +5,7 @@ const globals = require("globals");
|
||||
module.exports = defineConfig([
|
||||
{
|
||||
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
|
||||
ignores: ["**/*.config.js"],
|
||||
ignores: ["**/*.config.js", "**/public/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
@@ -13,7 +13,8 @@ module.exports = defineConfig([
|
||||
global: false,
|
||||
require: false,
|
||||
Buffer: false,
|
||||
process: false
|
||||
process: false,
|
||||
ipcRenderer: false
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
@@ -39,8 +40,60 @@ module.exports = defineConfig([
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-electron/**/*.{js}"],
|
||||
files: ["packages/bruno-cli/**/*.js"],
|
||||
ignores: ["**/*.config.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest"
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-common/**/*.ts"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parser: require("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./packages/bruno-common/tsconfig.json",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-converters/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-electron/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/web/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
@@ -50,5 +103,98 @@ module.exports = defineConfig([
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-filestore/**/*.ts"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parser: require("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./packages/bruno-filestore/tsconfig.json",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-js/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
window: false,
|
||||
self: false,
|
||||
HTMLElement: false,
|
||||
typeDetectGlobalObject: false
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-lang/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-requests/**/*.ts"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parser: require("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./packages/bruno-requests/tsconfig.json",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-requests/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
]);
|
||||
2882
package-lock.json
generated
2882
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.14.1",
|
||||
"@typescript-eslint/parser": "^8.39.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^9.26.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
@@ -76,4 +77,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,7 @@ const ImportEnvironment = ({ onClose }) => {
|
||||
}
|
||||
)
|
||||
.map((environment) => {
|
||||
let variables = environment?.variables?.map(v => ({
|
||||
...v,
|
||||
uid: uuid(),
|
||||
type: 'text'
|
||||
}));
|
||||
dispatch(addGlobalEnvironment({ name: environment.name, variables }))
|
||||
dispatch(addGlobalEnvironment({ name: environment.name, variables: environment.variables }))
|
||||
.then(() => {
|
||||
toast.success('Global Environment imported successfully');
|
||||
})
|
||||
|
||||
@@ -80,10 +80,6 @@ const QueryResult = ({ item, collection, data, dataBuffer, disableRunEventListen
|
||||
const [filter, setFilter] = useState(null);
|
||||
const [showLargeResponse, setShowLargeResponse] = useState(false);
|
||||
const responseEncoding = getEncoding(headers);
|
||||
const formattedData = useMemo(
|
||||
() => formatResponse(data, dataBuffer, responseEncoding, mode, filter),
|
||||
[data, dataBuffer, responseEncoding, mode, filter]
|
||||
);
|
||||
const { displayedTheme } = useTheme();
|
||||
|
||||
const responseSize = useMemo(() => {
|
||||
@@ -105,6 +101,16 @@ const QueryResult = ({ item, collection, data, dataBuffer, disableRunEventListen
|
||||
|
||||
const isLargeResponse = responseSize > 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
const formattedData = useMemo(
|
||||
() => {
|
||||
if (isLargeResponse && !showLargeResponse) {
|
||||
return '';
|
||||
}
|
||||
return formatResponse(data, dataBuffer, responseEncoding, mode, filter);
|
||||
},
|
||||
[data, dataBuffer, responseEncoding, mode, filter, isLargeResponse, showLargeResponse]
|
||||
);
|
||||
|
||||
const debouncedResultFilterOnChange = debounce((e) => {
|
||||
setFilter(e.target.value);
|
||||
}, 250);
|
||||
|
||||
@@ -37,7 +37,9 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
const requestUrl =
|
||||
get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
|
||||
const variables = getAllVariables(collection, item);
|
||||
const variables = useMemo(() => {
|
||||
return getAllVariables({ ...collection, globalEnvironmentVariables }, item);
|
||||
}, [collection, globalEnvironmentVariables, item]);
|
||||
|
||||
const interpolatedUrl = interpolateUrl({
|
||||
url: requestUrl,
|
||||
|
||||
@@ -5,10 +5,10 @@ const KeyMapping = {
|
||||
newRequest: { mac: 'command+b', windows: 'ctrl+b', name: 'New Request' },
|
||||
closeTab: { mac: 'command+w', windows: 'ctrl+w', name: 'Close Tab' },
|
||||
openPreferences: { mac: 'command+,', windows: 'ctrl+,', name: 'Open Preferences' },
|
||||
closeBruno: {
|
||||
mac: 'command+Q',
|
||||
windows: 'ctrl+shift+q',
|
||||
name: 'Close Bruno'
|
||||
minimizeWindow: {
|
||||
mac: 'command+Shift+Q',
|
||||
windows: 'control+Shift+Q',
|
||||
name: 'Minimize Window'
|
||||
},
|
||||
switchToPreviousTab: {
|
||||
mac: 'command+pageup',
|
||||
|
||||
@@ -41,7 +41,8 @@ import {
|
||||
initRunRequestEvent,
|
||||
updateRunnerConfiguration as _updateRunnerConfiguration,
|
||||
updateActiveConnections,
|
||||
saveRequest as _saveRequest
|
||||
saveRequest as _saveRequest,
|
||||
saveEnvironment as _saveEnvironment
|
||||
} from './index';
|
||||
|
||||
import { each } from 'lodash';
|
||||
@@ -59,6 +60,7 @@ import {
|
||||
calculateDraggedItemNewPathname
|
||||
} from 'utils/collections/index';
|
||||
import { sanitizeName } from 'utils/common/regex';
|
||||
import { buildPersistedEnvVariables } from 'utils/environments';
|
||||
import { safeParseJSON, safeStringifyJSON } from 'utils/common/index';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { updateSettingsSelectedTab } from './index';
|
||||
@@ -1167,8 +1169,16 @@ export const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, g
|
||||
const sanitizedName = sanitizeName(name);
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
// strip "ephemeral" metadata
|
||||
const variablesToCopy = (baseEnv.variables || [])
|
||||
.filter((v) => !v.ephemeral)
|
||||
.map(({ ephemeral, ...rest }) => {
|
||||
return rest;
|
||||
});
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:create-environment', collection.pathname, sanitizedName, baseEnv.variables)
|
||||
.invoke('renderer:create-environment', collection.pathname, sanitizedName, variablesToCopy)
|
||||
.then(
|
||||
dispatch(
|
||||
updateLastAction({
|
||||
@@ -1249,12 +1259,27 @@ export const saveEnvironment = (variables, environmentUid, collectionUid) => (di
|
||||
return reject(new Error('Environment not found'));
|
||||
}
|
||||
|
||||
environment.variables = variables;
|
||||
/*
|
||||
Modal Save writes what the user sees:
|
||||
- Non-ephemeral vars are saved as-is (without metadata)
|
||||
- Ephemeral vars:
|
||||
- if persistedValue exists, save that (explicit persisted case)
|
||||
- otherwise save the current UI value (treat as user-authored)
|
||||
*/
|
||||
const persisted = buildPersistedEnvVariables(variables, { mode: 'save' });
|
||||
environment.variables = persisted;
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
const envForValidation = cloneDeep(environment);
|
||||
|
||||
environmentSchema
|
||||
.validate(environment)
|
||||
.then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environment))
|
||||
.then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, envForValidation))
|
||||
.then(() => {
|
||||
// Immediately sync Redux to the saved (persisted) set so old ephemerals
|
||||
// aren’t around when the watcher event arrives.
|
||||
dispatch(_saveEnvironment({ variables: persisted, environmentUid, collectionUid }));
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
@@ -1311,12 +1336,15 @@ export const mergeAndPersistEnvironment =
|
||||
}
|
||||
});
|
||||
|
||||
environment.variables = merged;
|
||||
// Save only non-ephemeral vars, or ephemerals explicitly persisted this run
|
||||
const persistedNames = new Set(Object.keys(persistentEnvVariables));
|
||||
const environmentToSave = cloneDeep(environment);
|
||||
environmentToSave.variables = buildPersistedEnvVariables(merged, { mode: 'merge', persistedNames });
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
environmentSchema
|
||||
.validate(environment)
|
||||
.then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environment))
|
||||
.validate(environmentToSave)
|
||||
.then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environmentToSave))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
@@ -284,7 +284,20 @@ export const collectionsSlice = createSlice({
|
||||
const variable = find(activeEnvironment.variables, (v) => v.name === key);
|
||||
|
||||
if (variable) {
|
||||
variable.value = value;
|
||||
// For updates coming from scripts, treat them as ephemeral overlays.
|
||||
if (variable.value !== value) {
|
||||
/*
|
||||
Overlay (persist: false): keep new value in Redux for UI and mark ephemeral
|
||||
so it isn't written to disk. persistedValue stores the previous on-disk value;
|
||||
save/persist uses that base unless the key is explicitly persisted.
|
||||
*/
|
||||
const previousValue = variable.value;
|
||||
variable.value = value;
|
||||
variable.ephemeral = true;
|
||||
if (variable.persistedValue === undefined) {
|
||||
variable.persistedValue = previousValue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// __name__ is a private variable used to store the name of the environment
|
||||
// this is not a user defined variable and hence should not be updated
|
||||
@@ -295,7 +308,8 @@ export const collectionsSlice = createSlice({
|
||||
secret: false,
|
||||
enabled: true,
|
||||
type: 'text',
|
||||
uid: uuid()
|
||||
uid: uuid(),
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2275,7 +2289,21 @@ export const collectionsSlice = createSlice({
|
||||
const existingEnv = collection.environments.find((e) => e.uid === environment.uid);
|
||||
|
||||
if (existingEnv) {
|
||||
const prevEphemerals = (existingEnv.variables || []).filter((v) => v.ephemeral);
|
||||
existingEnv.variables = environment.variables;
|
||||
/*
|
||||
Apply temporary (ephemeral) values only to variables that actually exist in the file. This prevents deleted temporaries from “popping back” after a save. If a variable is present in the file, we temporarily override the UI value while also remembering the on-disk value in persistedValue for future saves.
|
||||
*/
|
||||
prevEphemerals.forEach((ev) => {
|
||||
const target = existingEnv.variables?.find((v) => v.name === ev.name);
|
||||
if (target) {
|
||||
if (target.value !== ev.value) {
|
||||
if (target.persistedValue === undefined) target.persistedValue = target.value;
|
||||
target.value = ev.value;
|
||||
}
|
||||
target.ephemeral = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
collection.environments.push(environment);
|
||||
collection.environments.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
@@ -53,14 +53,7 @@ const createQuery = (queryParams = []) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const createPostData = (body, type) => {
|
||||
if (type === 'graphql-request') {
|
||||
return {
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(body[body.mode])
|
||||
};
|
||||
}
|
||||
|
||||
const createPostData = (body) => {
|
||||
const contentType = createContentType(body.mode);
|
||||
|
||||
switch (body.mode) {
|
||||
@@ -112,6 +105,11 @@ const createPostData = (body, type) => {
|
||||
: []
|
||||
};
|
||||
}
|
||||
case 'graphql':
|
||||
return {
|
||||
mimeType: contentType,
|
||||
text: JSON.stringify(body[body.mode])
|
||||
};
|
||||
default:
|
||||
return {
|
||||
mimeType: contentType,
|
||||
@@ -120,7 +118,7 @@ const createPostData = (body, type) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const buildHarRequest = ({ request, headers, type }) => {
|
||||
export const buildHarRequest = ({ request, headers }) => {
|
||||
return {
|
||||
method: request.method,
|
||||
url: encodeURI(request.url),
|
||||
@@ -128,7 +126,7 @@ export const buildHarRequest = ({ request, headers, type }) => {
|
||||
cookies: [],
|
||||
headers: createHeaders(request, headers),
|
||||
queryString: createQuery(request.params),
|
||||
postData: createPostData(request.body, type),
|
||||
postData: createPostData(request.body),
|
||||
headersSize: 0,
|
||||
bodySize: 0,
|
||||
binary: true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash';
|
||||
import { uuid } from 'utils/common';
|
||||
import { buildPersistedEnvVariables } from 'utils/environments';
|
||||
import { sortByNameThenSequence } from 'utils/common/index';
|
||||
import path from 'utils/common/path';
|
||||
import { isRequestTagsIncluded } from '@usebruno/common';
|
||||
@@ -232,6 +233,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
return;
|
||||
}
|
||||
|
||||
const isGrpcRequest = si.type === 'grpc-request'
|
||||
|
||||
const di = {
|
||||
uid: si.uid,
|
||||
type: si.type,
|
||||
@@ -246,8 +249,6 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
di.request = {
|
||||
url: si.request.url,
|
||||
method: si.request.method,
|
||||
methodType: si.request.methodType,
|
||||
protoPath: si.request.protoPath,
|
||||
headers: copyHeaders(si.request.headers),
|
||||
params: copyParams(si.request.params),
|
||||
body: {
|
||||
@@ -269,6 +270,13 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
docs: si.request.docs
|
||||
};
|
||||
|
||||
if (isGrpcRequest) {
|
||||
di.request.methodType = si.request.methodType;
|
||||
di.request.protoPath = si.request.protoPath;
|
||||
delete di.request.params;
|
||||
}
|
||||
|
||||
|
||||
// Handle auth object dynamically
|
||||
di.request.auth = {
|
||||
mode: get(si.request, 'auth.mode', 'none')
|
||||
@@ -329,6 +337,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
additionalParameters: get(si.request, 'auth.oauth2.additionalParameters', {}),
|
||||
};
|
||||
break;
|
||||
case 'authorization_code':
|
||||
@@ -349,6 +358,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
additionalParameters: get(si.request, 'auth.oauth2.additionalParameters', {}),
|
||||
};
|
||||
break;
|
||||
case 'implicit':
|
||||
@@ -364,6 +374,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
additionalParameters: get(si.request, 'auth.oauth2.additionalParameters', {}),
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
@@ -381,6 +392,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
additionalParameters: get(si.request, 'auth.oauth2.additionalParameters', {}),
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -495,7 +507,11 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
collectionToSave.version = '1';
|
||||
collectionToSave.items = [];
|
||||
collectionToSave.activeEnvironmentUid = collection.activeEnvironmentUid;
|
||||
collectionToSave.environments = collection.environments || [];
|
||||
// Save environments without runtime metadata (ephemeral/persistedValue)
|
||||
collectionToSave.environments = (collection.environments || []).map((env) => ({
|
||||
...env,
|
||||
variables: buildPersistedEnvVariables(env?.variables, { mode: 'save' })
|
||||
}));
|
||||
|
||||
collectionToSave.root = {
|
||||
request: {}
|
||||
|
||||
31
packages/bruno-app/src/utils/environments.js
Normal file
31
packages/bruno-app/src/utils/environments.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const isPersistableEnvVarForMerge = (persistedNames) => (v) => {
|
||||
return !v?.ephemeral || v?.persistedValue !== undefined || (v?.name && persistedNames.has(v.name));
|
||||
};
|
||||
|
||||
const toPersistedEnvVarForMerge = (persistedNames) => (v) => {
|
||||
const { ephemeral, persistedValue, ...rest } = v || {};
|
||||
if (v?.ephemeral && persistedValue !== undefined && !(v?.name && persistedNames.has(v.name))) {
|
||||
return { ...rest, value: persistedValue };
|
||||
}
|
||||
return rest;
|
||||
};
|
||||
|
||||
const toPersistedEnvVarForSave = (v) => {
|
||||
const { ephemeral, persistedValue, ...rest } = v || {};
|
||||
return v?.ephemeral ? (persistedValue !== undefined ? { ...rest, value: persistedValue } : rest) : rest;
|
||||
};
|
||||
|
||||
/*
|
||||
High-level builder for persisted variables
|
||||
- mode 'save': write what the user sees
|
||||
- mode 'merge': write only allowed vars (non-ephemeral, ephemerals with persistedValue, or explicitly persisted this run)
|
||||
*/
|
||||
export const buildPersistedEnvVariables = (variables, { mode, persistedNames } = {}) => {
|
||||
const src = Array.isArray(variables) ? variables : [];
|
||||
if (mode === 'merge') {
|
||||
const names = persistedNames instanceof Set ? persistedNames : new Set();
|
||||
return src.filter(isPersistableEnvVarForMerge(names)).map(toPersistedEnvVarForMerge(names));
|
||||
}
|
||||
// default to save mode
|
||||
return src.map(toPersistedEnvVarForSave);
|
||||
};
|
||||
@@ -64,6 +64,7 @@ export const transformItemsInCollection = (collection) => {
|
||||
each(items, (item) => {
|
||||
if (['http', 'graphql', 'grpc'].includes(item.type)) {
|
||||
item.type = `${item.type}-request`;
|
||||
const isGrpcRequest = item.type === 'grpc-request';
|
||||
|
||||
if (item.request.query) {
|
||||
item.request.params = item.request.query.map((queryItem) => ({
|
||||
@@ -73,6 +74,10 @@ export const transformItemsInCollection = (collection) => {
|
||||
}));
|
||||
}
|
||||
|
||||
if (isGrpcRequest) {
|
||||
delete item.request.params;
|
||||
}
|
||||
|
||||
delete item.request.query;
|
||||
|
||||
// from 5 feb 2024, multipartFormData needs to have a type
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import { transformCollectionToSaveToExportAsFile, transformRequestToSaveToFilesystem } from '../../collections/index';
|
||||
import { transformItemsInCollection } from '../../importers/common';
|
||||
|
||||
describe('gRPC Export/Import', () => {
|
||||
describe('transformCollectionToSaveToExportAsFile', () => {
|
||||
it('should preserve gRPC-specific fields when exporting collection', () => {
|
||||
const collection = {
|
||||
uid: 'test-collection',
|
||||
name: 'Test Collection',
|
||||
items: [
|
||||
{
|
||||
uid: 'grpc-request-1',
|
||||
type: 'grpc-request',
|
||||
name: 'Test gRPC Request',
|
||||
request: {
|
||||
url: 'grpc://localhost:50051',
|
||||
method: '/randomService/randomMethod',
|
||||
methodType: 'unary',
|
||||
protoPath: 'proto/service.proto',
|
||||
headers: [],
|
||||
body: {
|
||||
mode: 'grpc',
|
||||
grpc: [{ name: 'message', content: '{}' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = transformCollectionToSaveToExportAsFile(collection);
|
||||
const grpcRequest = result.items[0];
|
||||
|
||||
expect(grpcRequest.request.methodType).toBe('unary');
|
||||
expect(grpcRequest.request.method).toBe('/randomService/randomMethod');
|
||||
expect(grpcRequest.request.protoPath).toBe('proto/service.proto');
|
||||
expect(grpcRequest.request.params).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle different gRPC method types correctly', () => {
|
||||
const collection = {
|
||||
uid: 'test-collection',
|
||||
name: 'Test Collection',
|
||||
items: [
|
||||
{
|
||||
uid: 'grpc-request-1',
|
||||
type: 'grpc-request',
|
||||
name: 'Streaming Request',
|
||||
request: {
|
||||
url: 'grpc://localhost:50051',
|
||||
method: '/randomService/randomMethod',
|
||||
methodType: 'bidi-streaming',
|
||||
protoPath: 'proto/streaming.proto',
|
||||
headers: [],
|
||||
body: { mode: 'grpc', grpc: [] }
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = transformCollectionToSaveToExportAsFile(collection);
|
||||
const grpcRequest = result.items[0];
|
||||
|
||||
expect(grpcRequest.request.methodType).toBe('bidi-streaming');
|
||||
expect(grpcRequest.request.method).toBe('/randomService/randomMethod');
|
||||
expect(grpcRequest.request.protoPath).toBe('proto/streaming.proto');
|
||||
});
|
||||
|
||||
it('should handle gRPC requests without method', () => {
|
||||
const collection = {
|
||||
uid: 'test-collection',
|
||||
name: 'Test Collection',
|
||||
items: [
|
||||
{
|
||||
uid: 'grpc-request-1',
|
||||
type: 'grpc-request',
|
||||
name: 'Streaming Request',
|
||||
request: {
|
||||
url: 'grpc://localhost:50051',
|
||||
methodType: 'unary',
|
||||
headers: [],
|
||||
body: { mode: 'grpc', grpc: [] }
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = transformCollectionToSaveToExportAsFile(collection);
|
||||
const grpcRequest = result.items[0];
|
||||
|
||||
expect(grpcRequest.request.methodType).toBe('unary');
|
||||
expect(grpcRequest.request.method).toBeUndefined();
|
||||
expect(grpcRequest.request.protoPath).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformRequestToSaveToFilesystem', () => {
|
||||
it('should preserve gRPC fields and remove params for gRPC requests', () => {
|
||||
const grpcRequest = {
|
||||
uid: 'grpc-request-1',
|
||||
type: 'grpc-request',
|
||||
name: 'Test gRPC',
|
||||
request: {
|
||||
url: 'grpc://localhost:50051',
|
||||
method: '/randomService/randomMethod',
|
||||
methodType: 'server-streaming',
|
||||
protoPath: 'proto/service.proto',
|
||||
params: [{ uid: 'param-1', name: 'test', value: 'value' }],
|
||||
headers: [],
|
||||
body: { mode: 'grpc', grpc: [] }
|
||||
}
|
||||
};
|
||||
|
||||
const result = transformRequestToSaveToFilesystem(grpcRequest);
|
||||
|
||||
expect(result.request.methodType).toBe('server-streaming');
|
||||
expect(result.request.protoPath).toBe('proto/service.proto');
|
||||
expect(result.request.params).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not remove params for non-gRPC requests', () => {
|
||||
const httpRequest = {
|
||||
uid: 'http-request-1',
|
||||
type: 'http-request',
|
||||
name: 'Test HTTP',
|
||||
request: {
|
||||
url: 'http://localhost:3000',
|
||||
method: 'GET',
|
||||
params: [{ uid: 'param-1', name: 'test', value: 'value' }],
|
||||
headers: [],
|
||||
body: { mode: 'json', json: '{}' }
|
||||
}
|
||||
};
|
||||
|
||||
const result = transformRequestToSaveToFilesystem(httpRequest);
|
||||
|
||||
expect(result.request.params).toHaveLength(1);
|
||||
expect(result.request.params[0].name).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformItemsInCollection', () => {
|
||||
it('should transform gRPC request type correctly during import', () => {
|
||||
const collection = {
|
||||
uid: 'test-collection',
|
||||
items: [
|
||||
{
|
||||
uid: 'grpc-request-1',
|
||||
type: 'grpc',
|
||||
name: 'Test gRPC',
|
||||
request: {
|
||||
url: 'grpc://localhost:50051',
|
||||
methodType: 'unary',
|
||||
protoPath: 'proto/service.proto',
|
||||
body: { mode: 'grpc', grpc: [] }
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
transformItemsInCollection(collection);
|
||||
const grpcRequest = collection.items[0];
|
||||
|
||||
expect(grpcRequest.type).toBe('grpc-request');
|
||||
expect(grpcRequest.request.methodType).toBe('unary');
|
||||
expect(grpcRequest.request.protoPath).toBe('proto/service.proto');
|
||||
});
|
||||
|
||||
it('should handle gRPC requests without protoPath', () => {
|
||||
const collection = {
|
||||
uid: 'test-collection',
|
||||
items: [
|
||||
{
|
||||
uid: 'grpc-request-1',
|
||||
type: 'grpc',
|
||||
name: 'Test gRPC',
|
||||
request: {
|
||||
url: 'grpc://localhost:50051',
|
||||
method: '/randomService/randomMethod',
|
||||
methodType: 'client-streaming',
|
||||
body: { mode: 'grpc', grpc: [] }
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
transformItemsInCollection(collection);
|
||||
const grpcRequest = collection.items[0];
|
||||
|
||||
expect(grpcRequest.type).toBe('grpc-request');
|
||||
expect(grpcRequest.request.methodType).toBe('client-streaming');
|
||||
expect(grpcRequest.request.protoPath).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle gRPC requests without method', () => {
|
||||
const collection = {
|
||||
uid: 'test-collection',
|
||||
items: [
|
||||
{
|
||||
uid: 'grpc-request-1',
|
||||
type: 'grpc',
|
||||
name: 'Test gRPC',
|
||||
request: {
|
||||
url: 'grpc://localhost:50051',
|
||||
methodType: 'unary',
|
||||
protoPath: 'proto/service.proto',
|
||||
body: { mode: 'grpc', grpc: [] }
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
transformItemsInCollection(collection);
|
||||
const grpcRequest = collection.items[0];
|
||||
|
||||
expect(grpcRequest.type).toBe('grpc-request');
|
||||
expect(grpcRequest.request.method).toBeUndefined();
|
||||
expect(grpcRequest.request.methodType).toBe('unary');
|
||||
expect(grpcRequest.request.protoPath).toBe('proto/service.proto');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,11 +9,11 @@ async function resolveAwsV4Credentials(request) {
|
||||
const awsv4 = request.awsv4config;
|
||||
if (isStrPresent(awsv4.profileName)) {
|
||||
try {
|
||||
credentialsProvider = fromIni({
|
||||
const credentialsProvider = fromIni({
|
||||
profile: awsv4.profileName,
|
||||
ignoreCache: true
|
||||
});
|
||||
credentials = await credentialsProvider();
|
||||
const credentials = await credentialsProvider();
|
||||
awsv4.accessKeyId = credentials.accessKeyId;
|
||||
awsv4.secretAccessKey = credentials.secretAccessKey;
|
||||
awsv4.sessionToken = credentials.sessionToken;
|
||||
|
||||
@@ -476,29 +476,24 @@ const processCollectionItems = async (items = [], currentPath) => {
|
||||
// Convert JSON to BRU format based on the item type
|
||||
let type = item.type === 'http-request' ? 'http' : 'graphql';
|
||||
const bruJson = {
|
||||
meta: {
|
||||
name: item.name,
|
||||
type: type,
|
||||
seq: typeof item.seq === 'number' ? item.seq : 1
|
||||
},
|
||||
http: {
|
||||
method: (item.request?.method || 'GET').toLowerCase(),
|
||||
type: type,
|
||||
name: item.name,
|
||||
seq: typeof item.seq === 'number' ? item.seq : 1,
|
||||
tags: item.tags || [],
|
||||
settings: {},
|
||||
request: {
|
||||
method: item.request?.method || 'GET',
|
||||
url: item.request?.url || '',
|
||||
auth: item.request?.auth?.mode || 'none',
|
||||
body: item.request?.body?.mode || 'none'
|
||||
},
|
||||
params: item.request?.params || [],
|
||||
headers: item.request?.headers || [],
|
||||
auth: item.request?.auth || {},
|
||||
body: item.request?.body || {},
|
||||
script: item.request?.script || {},
|
||||
vars: {
|
||||
req: item.request?.vars?.req || [],
|
||||
res: item.request?.vars?.res || []
|
||||
},
|
||||
assertions: item.request?.assertions || [],
|
||||
tests: item.request?.tests || '',
|
||||
docs: item.request?.docs || ''
|
||||
headers: item.request?.headers || [],
|
||||
params: item.request?.params || [],
|
||||
auth: item.request?.auth || {},
|
||||
body: item.request?.body || {},
|
||||
script: item.request?.script || {},
|
||||
vars: item.request?.vars || { req: [], res: [] },
|
||||
assertions: item.request?.assertions || [],
|
||||
tests: item.request?.tests || '',
|
||||
docs: item.request?.docs || ''
|
||||
}
|
||||
};
|
||||
|
||||
// Convert to BRU format and write to file
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = require('@usebruno/common').cookies;
|
||||
module.exports = require('@usebruno/requests').cookies;
|
||||
|
||||
@@ -56,8 +56,5 @@
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.29.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"tough-cookie": "^6.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export { mockDataFunctions } from './utils/faker-functions';
|
||||
export { default as interpolate } from './interpolate';
|
||||
export { default as isRequestTagsIncluded } from './tags';
|
||||
export { default as cookies } from './cookies';
|
||||
|
||||
export * as utils from './utils';
|
||||
@@ -2,8 +2,4 @@ export {
|
||||
encodeUrl,
|
||||
parseQueryParams,
|
||||
buildQueryString,
|
||||
} from './url';
|
||||
|
||||
export {
|
||||
isPotentiallyTrustworthyOrigin
|
||||
} from './url/validation';
|
||||
} from './url';
|
||||
@@ -15,6 +15,7 @@ const importPostmanEnvironmentVariables = (brunoEnvironment, values = []) => {
|
||||
name: (i.key ?? '').replace(invalidVariableCharacterRegex, '_'),
|
||||
value: i.value ?? '',
|
||||
enabled: i.enabled,
|
||||
type: 'text',
|
||||
secret: isSecret(i.type)
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('postmanToBrunoEnvironment Function', () => {
|
||||
value: 'value1',
|
||||
enabled: true,
|
||||
secret: false,
|
||||
type: 'text',
|
||||
uid: "mockeduuidvalue123456",
|
||||
},
|
||||
{
|
||||
@@ -39,6 +40,7 @@ describe('postmanToBrunoEnvironment Function', () => {
|
||||
value: 'value2',
|
||||
enabled: false,
|
||||
secret: true,
|
||||
type: 'text',
|
||||
uid: "mockeduuidvalue123456",
|
||||
},
|
||||
],
|
||||
@@ -85,6 +87,7 @@ describe('postmanToBrunoEnvironment Function', () => {
|
||||
value: '',
|
||||
enabled: true,
|
||||
secret: false,
|
||||
type: 'text',
|
||||
uid: "mockeduuidvalue123456",
|
||||
},
|
||||
{
|
||||
@@ -92,6 +95,7 @@ describe('postmanToBrunoEnvironment Function', () => {
|
||||
value: '',
|
||||
enabled: true,
|
||||
secret: false,
|
||||
type: 'text',
|
||||
uid: "mockeduuidvalue123456",
|
||||
},
|
||||
{
|
||||
@@ -99,7 +103,8 @@ describe('postmanToBrunoEnvironment Function', () => {
|
||||
value: '',
|
||||
enabled: true,
|
||||
secret: false,
|
||||
uid: "mockeduuidvalue123456",
|
||||
type: 'text',
|
||||
uid: "mockeduuidvalue123456"
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ require('dotenv').config({ path: process.env.DOTENV_PATH });
|
||||
const config = {
|
||||
appId: 'com.usebruno.app',
|
||||
productName: 'Bruno',
|
||||
electronVersion: '33.2.1',
|
||||
electronVersion: '37.2.6',
|
||||
directories: {
|
||||
buildResources: 'resources',
|
||||
output: 'out'
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "~37.2.6",
|
||||
"electron-builder": "25.1.8",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-devtools-installer": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ if (isDev) {
|
||||
}
|
||||
|
||||
const { format } = require('url');
|
||||
const { BrowserWindow, app, session, Menu, globalShortcut, ipcMain } = require('electron');
|
||||
const { BrowserWindow, app, session, Menu, ipcMain } = require('electron');
|
||||
const { setContentSecurityPolicy } = require('electron-util');
|
||||
|
||||
if (isDev && process.env.ELECTRON_USER_DATA_PATH) {
|
||||
@@ -168,19 +168,6 @@ app.on('ready', async () => {
|
||||
}
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
// Quick fix for Electron issue #29996: https://github.com/electron/electron/issues/29996
|
||||
globalShortcut.register('Ctrl+=', () => {
|
||||
mainWindow.webContents.setZoomLevel(mainWindow.webContents.getZoomLevel() + 1);
|
||||
});
|
||||
|
||||
globalShortcut.register('CommandOrControl+M', () => {
|
||||
mainWindow.minimize();
|
||||
});
|
||||
|
||||
globalShortcut.register('CommandOrControl+H', () => {
|
||||
mainWindow.minimize();
|
||||
});
|
||||
|
||||
mainWindow.webContents.on('did-finish-load', async () => {
|
||||
let ogSend = mainWindow.webContents.send;
|
||||
|
||||
@@ -9,11 +9,11 @@ async function resolveAwsV4Credentials(request) {
|
||||
const awsv4 = request.awsv4config;
|
||||
if (isStrPresent(awsv4.profileName)) {
|
||||
try {
|
||||
credentialsProvider = fromIni({
|
||||
const credentialsProvider = fromIni({
|
||||
profile: awsv4.profileName,
|
||||
ignoreCache: true
|
||||
});
|
||||
credentials = await credentialsProvider();
|
||||
const credentials = await credentialsProvider();
|
||||
awsv4.accessKeyId = credentials.accessKeyId;
|
||||
awsv4.secretAccessKey = credentials.secretAccessKey;
|
||||
awsv4.sessionToken = credentials.sessionToken;
|
||||
|
||||
@@ -129,6 +129,7 @@ const prepareRequest = async (item, collection, environment, runtimeVariables, c
|
||||
const collectionRoot = collection?.draft ? get(collection, 'draft', {}) : get(collection, 'root', {});
|
||||
const headers = {};
|
||||
const url = request.url;
|
||||
let contentTypeDefined = false;
|
||||
|
||||
each(get(collectionRoot, 'request.headers', []), (h) => {
|
||||
if (h.enabled && h.name?.toLowerCase() === 'content-type') {
|
||||
@@ -186,7 +187,7 @@ const prepareRequest = async (item, collection, environment, runtimeVariables, c
|
||||
if (grpcRequest.oauth2) {
|
||||
let requestCopy = cloneDeep(grpcRequest);
|
||||
const { oauth2: { grantType, tokenPlacement, tokenHeaderPrefix, tokenQueryKey } = {} } = requestCopy || {};
|
||||
let credentials, credentialsId;
|
||||
let credentials, credentialsId, oauth2Url, debugInfo;
|
||||
switch (grantType) {
|
||||
case 'authorization_code':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
|
||||
@@ -102,7 +102,7 @@ const configureRequest = async (
|
||||
if (request.oauth2) {
|
||||
let requestCopy = cloneDeep(request);
|
||||
const { oauth2: { grantType, tokenPlacement, tokenHeaderPrefix, tokenQueryKey } = {} } = requestCopy || {};
|
||||
let credentials, credentialsId;
|
||||
let credentials, credentialsId, oauth2Url, debugInfo;
|
||||
switch (grantType) {
|
||||
case 'authorization_code':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const Store = require('electron-store');
|
||||
const { cookies: cookiesModule } = require('@usebruno/common');
|
||||
const { cookies: cookiesModule } = require('@usebruno/requests');
|
||||
const { cookieJar } = cookiesModule;
|
||||
const { Cookie } = require('tough-cookie');
|
||||
const { createCookieString } = cookiesModule;
|
||||
|
||||
@@ -41,6 +41,18 @@ class GlobalEnvironmentsStore {
|
||||
getGlobalEnvironments() {
|
||||
let globalEnvironments = this.store.get('environments', []);
|
||||
globalEnvironments = this.decryptGlobalEnvironmentVariables({ globalEnvironments });
|
||||
|
||||
// Previously, a bug caused environment variables to be saved without a type.
|
||||
// Since that issue is now fixed, this code ensures that anyone who imported
|
||||
// data before the fix will have the missing types added retroactively.
|
||||
globalEnvironments?.forEach(env => {
|
||||
env?.variables?.forEach(v => {
|
||||
if (!v.type) {
|
||||
v.type = 'text';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return globalEnvironments;
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = require('@usebruno/common').cookies;
|
||||
module.exports = require('@usebruno/requests').cookies;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
const { globalEnvironmentsStore } = require('../../src/store/global-environments');
|
||||
|
||||
// Previously, a bug caused environment variables to be saved without a type.
|
||||
// Since that issue is now fixed, this code ensures that anyone who imported
|
||||
// data before the fix will have the missing types added retroactively.
|
||||
describe('global environment variable type backward compatibility', () => {
|
||||
beforeEach(() => {
|
||||
globalEnvironmentsStore.store.clear();
|
||||
});
|
||||
|
||||
it('should add type field for existing global environments without type', () => {
|
||||
// Mock global environments without type field
|
||||
const mockGlobalEnvironments = [
|
||||
{
|
||||
uid: "env-1",
|
||||
name: "Test Environment",
|
||||
variables: [
|
||||
{
|
||||
uid: "var-1",
|
||||
name: "regular_var",
|
||||
value: "regular_value",
|
||||
enabled: true,
|
||||
secret: false
|
||||
// Missing: type field
|
||||
},
|
||||
{
|
||||
uid: "var-2",
|
||||
name: "secret_var",
|
||||
value: "secret_value",
|
||||
enabled: true,
|
||||
secret: true
|
||||
// Missing: type field
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
globalEnvironmentsStore.store.set('environments', mockGlobalEnvironments);
|
||||
|
||||
const processedEnvironments = globalEnvironmentsStore.getGlobalEnvironments();
|
||||
|
||||
expect(processedEnvironments).toHaveLength(1);
|
||||
expect(processedEnvironments[0].variables).toHaveLength(2);
|
||||
|
||||
const regularVar = processedEnvironments[0].variables.find(v => v.name === 'regular_var');
|
||||
const secretVar = processedEnvironments[0].variables.find(v => v.name === 'secret_var');
|
||||
|
||||
expect(regularVar.name).toBe('regular_var');
|
||||
expect(regularVar.type).toBe('text');
|
||||
|
||||
expect(secretVar.name).toBe('secret_var');
|
||||
expect(secretVar.type).toBe('text');
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
const { cloneDeep } = require('lodash');
|
||||
const { interpolate: _interpolate } = require('@usebruno/common');
|
||||
const { sendRequest } = require('@usebruno/requests').scripting;
|
||||
const { jar: createCookieJar } = require('@usebruno/common').cookies;
|
||||
const { jar: createCookieJar } = require('@usebruno/requests').cookies;
|
||||
|
||||
const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
@@ -125,6 +125,12 @@ class Bru {
|
||||
throw new Error('Creating a env variable without specifying a name is not allowed.');
|
||||
}
|
||||
|
||||
if (variableNameRegex.test(key) === false) {
|
||||
throw new Error(
|
||||
`Variable name: "${key}" contains invalid characters! Names must only contain alpha-numeric characters, "-", "_", "."`
|
||||
);
|
||||
}
|
||||
|
||||
// When persist is true, only string values are allowed
|
||||
if (options?.persist && typeof value !== 'string') {
|
||||
throw new Error(`Persistent environment variables must be strings. Received ${typeof value} for key "${key}".`);
|
||||
@@ -133,7 +139,7 @@ class Bru {
|
||||
this.envVariables[key] = value;
|
||||
|
||||
if (options?.persist) {
|
||||
this.persistentEnvVariables[key] = value
|
||||
this.persistentEnvVariables[key] = value;
|
||||
} else {
|
||||
if (this.persistentEnvVariables[key]) {
|
||||
delete this.persistentEnvVariables[key];
|
||||
|
||||
74
packages/bruno-js/tests/setEnvVar.spec.js
Normal file
74
packages/bruno-js/tests/setEnvVar.spec.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const Bru = require('../src/bru');
|
||||
|
||||
describe('Bru.setEnvVar', () => {
|
||||
const makeBru = () =>
|
||||
new Bru(
|
||||
/* envVariables */ {},
|
||||
/* runtimeVariables */ {},
|
||||
/* processEnvVars */ {},
|
||||
/* collectionPath */ '/',
|
||||
/* historyLogger */ undefined,
|
||||
/* setVisualizations */ undefined,
|
||||
/* secretVariables */ {},
|
||||
/* collectionVariables */ {},
|
||||
/* folderVariables */ {},
|
||||
/* requestVariables */ {},
|
||||
/* globalEnvironmentVariables */ {},
|
||||
/* oauth2CredentialVariables */ {},
|
||||
/* iterationDetails */ {},
|
||||
/* collectionName */ 'Test'
|
||||
);
|
||||
|
||||
test('updates envVariables and does not mark persistent when persist=false', () => {
|
||||
const bru = makeBru();
|
||||
bru.setEnvVar('non_persist', 'value', { persist: false });
|
||||
expect(bru.envVariables.non_persist).toBe('value');
|
||||
expect(bru.persistentEnvVariables.non_persist).toBeUndefined();
|
||||
});
|
||||
|
||||
test('updates envVariables and tracks persistent when persist=true (string only)', () => {
|
||||
const bru = makeBru();
|
||||
bru.setEnvVar('persist_me', 'value', { persist: true });
|
||||
expect(bru.envVariables.persist_me).toBe('value');
|
||||
expect(bru.persistentEnvVariables.persist_me).toBe('value');
|
||||
});
|
||||
|
||||
test('updates envVariables when options are omitted (defaults to non-persistent)', () => {
|
||||
const bru = makeBru();
|
||||
bru.setEnvVar('no_options', 'value');
|
||||
expect(bru.envVariables.no_options).toBe('value');
|
||||
expect(bru.persistentEnvVariables.no_options).toBeUndefined();
|
||||
});
|
||||
|
||||
test('throws when persist=true but value is not a string', () => {
|
||||
const bru = makeBru();
|
||||
expect(() => bru.setEnvVar('persist_me', 123, { persist: true })).toThrow(
|
||||
/Persistent environment variables must be strings/
|
||||
);
|
||||
});
|
||||
|
||||
test('changing existing key to non-persistent removes prior persisted entry', () => {
|
||||
const bru = makeBru();
|
||||
bru.setEnvVar('same_key', 'old', { persist: true });
|
||||
expect(bru.persistentEnvVariables.same_key).toBe('old');
|
||||
|
||||
bru.setEnvVar('same_key', 'new');
|
||||
expect(bru.envVariables.same_key).toBe('new');
|
||||
expect(bru.persistentEnvVariables.same_key).toBeUndefined();
|
||||
});
|
||||
|
||||
test('changing existing key to persistent updates persisted value', () => {
|
||||
const bru = makeBru();
|
||||
bru.setEnvVar('same_key', 'old');
|
||||
expect(bru.persistentEnvVariables.same_key).toBeUndefined();
|
||||
|
||||
bru.setEnvVar('same_key', 'new', { persist: true });
|
||||
expect(bru.envVariables.same_key).toBe('new');
|
||||
expect(bru.persistentEnvVariables.same_key).toBe('new');
|
||||
});
|
||||
|
||||
test('validates key name - invalid characters are rejected', () => {
|
||||
const bru = makeBru();
|
||||
expect(() => bru.setEnvVar('invalid key', 'v')).toThrow(/contains invalid characters/);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ module.exports = {
|
||||
'^.+\\.(ts|js)$': 'babel-jest',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!(lodash-es)/)',
|
||||
'/node_modules/(?!(lodash-es|is-ip|ip-regex|super-regex|function-timeout|time-span|convert-hrtime|clone-regexp|is-regexp)/)'
|
||||
],
|
||||
testEnvironment: 'node',
|
||||
testMatch: [
|
||||
|
||||
@@ -24,8 +24,10 @@
|
||||
"@grpc/grpc-js": "^1.13.3",
|
||||
"@grpc/proto-loader": "^0.7.15",
|
||||
"@types/qs": "^6.9.18",
|
||||
"axios": "^1.9.0",
|
||||
"grpc-reflection-js": "^0.3.0",
|
||||
"axios": "^1.9.0"
|
||||
"is-ip": "^5.0.1",
|
||||
"tough-cookie": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.22.0",
|
||||
@@ -37,8 +39,8 @@
|
||||
"@rollup/plugin-typescript": "^9.0.2",
|
||||
"@types/jest": "^29.5.11",
|
||||
"babel-jest": "^29.7.0",
|
||||
"jest": "^29.2.0",
|
||||
"builtin-modules": "^5.0.0",
|
||||
"jest": "^29.2.0",
|
||||
"rollup": "3.29.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
@@ -48,4 +50,4 @@
|
||||
"overrides": {
|
||||
"rollup": "3.29.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
const cookiesModule = require('../../src/cookies/index.ts').default;
|
||||
import cookiesModule from './index';
|
||||
import { Cookie } from 'tough-cookie';
|
||||
|
||||
// Provide explicit type for the cookie-jar wrapper returned by cookiesModule.jar()
|
||||
type CookieJarWrapper = ReturnType<typeof cookiesModule.jar>;
|
||||
|
||||
const jarFactory = (): CookieJarWrapper => cookiesModule.jar();
|
||||
|
||||
describe('Bruno Cookie Jar Wrapper - API Examples', () => {
|
||||
let jar;
|
||||
let jar: CookieJarWrapper;
|
||||
const testUrl = 'https://api.example.com';
|
||||
|
||||
beforeEach(() => {
|
||||
jar = cookiesModule.jar();
|
||||
jar = jarFactory();
|
||||
// Clear all cookies before each test
|
||||
jar.clear();
|
||||
});
|
||||
@@ -19,7 +25,7 @@ describe('Bruno Cookie Jar Wrapper - API Examples', () => {
|
||||
await jar.setCookie(testUrl, cookieName, cookieValue);
|
||||
|
||||
// Get the cookie back
|
||||
const cookie = await jar.getCookie(testUrl, cookieName);
|
||||
const cookie = (await jar.getCookie(testUrl, cookieName))!;
|
||||
expect(cookie.key).toBe(cookieName);
|
||||
expect(cookie.value).toBe(cookieValue);
|
||||
expect(cookie.domain).toBe('api.example.com');
|
||||
@@ -36,7 +42,7 @@ describe('Bruno Cookie Jar Wrapper - API Examples', () => {
|
||||
|
||||
await jar.setCookie(testUrl, cookieObj);
|
||||
|
||||
const cookie = await jar.getCookie(testUrl + '/api', 'sessionId');
|
||||
const cookie = (await jar.getCookie(testUrl + '/api', 'sessionId'))!;
|
||||
expect(cookie.key).toBe('sessionId');
|
||||
expect(cookie.value).toBe('abc123');
|
||||
expect(cookie.path).toBe('/api');
|
||||
@@ -61,10 +67,10 @@ describe('Bruno Cookie Jar Wrapper - API Examples', () => {
|
||||
await jar.setCookies(testUrl, cookies);
|
||||
|
||||
// Verify all cookies were set
|
||||
const retrievedCookies = await jar.getCookies(testUrl);
|
||||
const retrievedCookies = (await jar.getCookies(testUrl)) as Cookie[];
|
||||
expect(retrievedCookies).toHaveLength(3);
|
||||
|
||||
const cookieNames = retrievedCookies.map(c => c.key);
|
||||
const cookieNames = retrievedCookies.map((c: Cookie) => c.key);
|
||||
expect(cookieNames).toContain('cookie1');
|
||||
expect(cookieNames).toContain('cookie2');
|
||||
expect(cookieNames).toContain('cookie3');
|
||||
@@ -76,13 +82,13 @@ describe('Bruno Cookie Jar Wrapper - API Examples', () => {
|
||||
await jar.setCookie(testUrl, 'session', 'sess456');
|
||||
await jar.setCookie(testUrl, 'prefs', 'theme=dark');
|
||||
|
||||
const cookies = await jar.getCookies(testUrl);
|
||||
const cookies = (await jar.getCookies(testUrl)) as Cookie[];
|
||||
expect(cookies).toHaveLength(3);
|
||||
|
||||
const cookieMap = cookies.reduce((map, cookie) => {
|
||||
const cookieMap = (cookies as Cookie[]).reduce<Record<string, string>>((map, cookie: Cookie) => {
|
||||
map[cookie.key] = cookie.value;
|
||||
return map;
|
||||
}, {});
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
expect(cookieMap.auth).toBe('token123');
|
||||
expect(cookieMap.session).toBe('sess456');
|
||||
@@ -100,10 +106,10 @@ describe('Bruno Cookie Jar Wrapper - API Examples', () => {
|
||||
await jar.deleteCookie(testUrl, 'remove');
|
||||
|
||||
// Verify only one cookie remains
|
||||
const cookies = await jar.getCookies(testUrl);
|
||||
const cookies = (await jar.getCookies(testUrl)) as Cookie[];
|
||||
expect(cookies).toHaveLength(1);
|
||||
expect(cookies[0].key).toBe('keep');
|
||||
expect(cookies[0].value).toBe('keepValue');
|
||||
expect(cookies[0]!.key).toBe('keep');
|
||||
expect(cookies[0]!.value).toBe('keepValue');
|
||||
});
|
||||
|
||||
test('deleteCookies removes all cookies for URL', async () => {
|
||||
@@ -115,7 +121,7 @@ describe('Bruno Cookie Jar Wrapper - API Examples', () => {
|
||||
await jar.deleteCookies(testUrl);
|
||||
|
||||
// Verify no cookies remain
|
||||
const cookies = await jar.getCookies(testUrl);
|
||||
const cookies = (await jar.getCookies(testUrl)) as Cookie[];
|
||||
expect(cookies).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -128,8 +134,8 @@ describe('Bruno Cookie Jar Wrapper - API Examples', () => {
|
||||
await jar.clear();
|
||||
|
||||
// Verify no cookies remain for any URL
|
||||
const cookies1 = await jar.getCookies('https://site1.com');
|
||||
const cookies2 = await jar.getCookies('https://site2.com');
|
||||
const cookies1 = (await jar.getCookies('https://site1.com')) as Cookie[];
|
||||
const cookies2 = (await jar.getCookies('https://site2.com')) as Cookie[];
|
||||
|
||||
expect(cookies1).toHaveLength(0);
|
||||
expect(cookies2).toHaveLength(0);
|
||||
@@ -146,7 +152,7 @@ describe('Bruno Cookie Jar Wrapper - API Examples', () => {
|
||||
});
|
||||
|
||||
test('setCookies handles invalid input', async () => {
|
||||
await expect(jar.setCookies(testUrl, 'not-an-array')).rejects.toThrow('expects an array');
|
||||
await expect(jar.setCookies(testUrl, 'not-an-array' as any)).rejects.toThrow('expects an array');
|
||||
});
|
||||
|
||||
test('setCookie handles missing cookie name in object', async () => {
|
||||
@@ -163,7 +169,7 @@ describe('Bruno Cookie Jar Wrapper - API Examples', () => {
|
||||
await jar.setCookie(apiUrl, 'authToken', authToken);
|
||||
|
||||
// Later in the session - retrieve auth token
|
||||
const cookie = await jar.getCookie(apiUrl, 'authToken');
|
||||
const cookie = (await jar.getCookie(apiUrl, 'authToken'))!;
|
||||
expect(cookie.value).toBe(authToken);
|
||||
|
||||
// Simulate logout - remove auth cookie
|
||||
@@ -187,13 +193,13 @@ describe('Bruno Cookie Jar Wrapper - API Examples', () => {
|
||||
await jar.setCookies(sessionUrl, sessionCookies);
|
||||
|
||||
// Retrieve all session cookies
|
||||
const cookies = await jar.getCookies(sessionUrl);
|
||||
const cookies = (await jar.getCookies(sessionUrl)) as Cookie[];
|
||||
expect(cookies).toHaveLength(3);
|
||||
|
||||
// Find specific cookies
|
||||
const sessionCookie = cookies.find(c => c.key === 'sessionId');
|
||||
const csrfCookie = cookies.find(c => c.key === 'csrfToken');
|
||||
const prefsCookie = cookies.find(c => c.key === 'userPrefs');
|
||||
const sessionCookie = cookies.find((c: Cookie) => c.key === 'sessionId')!;
|
||||
const csrfCookie = cookies.find((c: Cookie) => c.key === 'csrfToken')!;
|
||||
const prefsCookie = cookies.find((c: Cookie) => c.key === 'userPrefs')!;
|
||||
|
||||
expect(sessionCookie.value).toBe('sess_123');
|
||||
expect(sessionCookie.httpOnly).toBe(true);
|
||||
@@ -212,15 +218,15 @@ describe('Bruno Cookie Jar Wrapper - API Examples', () => {
|
||||
await jar.setCookie(baseUrl, { key: 'api', value: 'api_val', path: '/api' });
|
||||
await jar.setCookie(baseUrl, { key: 'admin', value: 'admin_val', path: '/admin' });
|
||||
|
||||
const rootCookies = await jar.getCookies(baseUrl + '/');
|
||||
const globalCookie = rootCookies.find(c => c.key === 'global');
|
||||
const rootCookies = (await jar.getCookies(baseUrl + '/')) as Cookie[];
|
||||
const globalCookie = rootCookies.find((c: Cookie) => c.key === 'global')!;
|
||||
expect(globalCookie).toBeTruthy();
|
||||
expect(globalCookie.value).toBe('global_val');
|
||||
|
||||
const apiCookies = await jar.getCookies(baseUrl + '/api/users');
|
||||
const apiCookies = (await jar.getCookies(baseUrl + '/api/users')) as Cookie[];
|
||||
expect(apiCookies.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const apiCookieNames = apiCookies.map(c => c.key);
|
||||
const apiCookieNames = apiCookies.map((c: Cookie) => c.key);
|
||||
expect(apiCookieNames).toContain('global');
|
||||
expect(apiCookieNames).toContain('api');
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Cookie, CookieJar } from 'tough-cookie';
|
||||
import each from 'lodash/each';
|
||||
import moment from 'moment';
|
||||
import { isPotentiallyTrustworthyOrigin } from '../utils';
|
||||
import { isPotentiallyTrustworthyOrigin } from '../utils/url-validation';
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { addDigestInterceptor, getOAuth2Token } from './auth';
|
||||
export { GrpcClient, generateGrpcSampleMessage } from './grpc';
|
||||
export { default as cookies } from './cookies';
|
||||
|
||||
export * as network from './network';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isPotentiallyTrustworthyOrigin } from './validation';
|
||||
import { isPotentiallyTrustworthyOrigin } from './url-validation';
|
||||
|
||||
describe('isPotentiallyTrustworthyOrigin', () => {
|
||||
describe('secure schemes', () => {
|
||||
@@ -94,9 +94,9 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# On Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" | gpg --dearmor | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user