Compare commits

..

2 Commits

Author SHA1 Message Date
lohit
63aaaac517 fix(bru-2035): form-urlencoded logic updates (#5820) 2025-10-17 18:34:17 +05:30
Sid
a66e0a314b fix: improve URL parsing in getParsedWsUrlObject (#5822) 2025-10-17 18:34:09 +05:30
65 changed files with 101 additions and 1785 deletions

View File

@@ -25,7 +25,7 @@ jobs:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'

View File

@@ -15,7 +15,7 @@ jobs:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
@@ -44,7 +44,7 @@ jobs:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
@@ -73,7 +73,7 @@ jobs:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps

View File

@@ -13,7 +13,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
@@ -66,7 +66,7 @@ jobs:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
@@ -108,7 +108,7 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v5
with:
node-version: v22.11.x

View File

@@ -37,37 +37,13 @@ Bruno 直接在您的电脑文件夹中存储您的 API 信息。我们使用纯
Bruno 仅限离线使用。我们计划永不向 Bruno 添加云同步功能。我们重视您的数据隐私,并认为它应该留在您的设备上。阅读我们的长期愿景 [点击查看](https://github.com/usebruno/bruno/discussions/269)
[下载 Bruno](https://www.usebruno.com/downloads)
📢 观看我们在印度 FOSS 3.0 会议上的最新演讲 [点击查看](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](../../assets/images/landing-2.png) <br /><br />
## 商业版本 ✨
### 安装
我们的大多数功能都是免费且开源的
我们致力于在 [开源与可持续性发展](https://github.com/usebruno/bruno/discussions/269) 之间取得和谐的平衡
欢迎使用我们的 [付费版本](https://www.usebruno.com/pricing) ,看看附加的功能是否对您或团队有所帮助! <br/>
## 目录
- [安装](#安装)
- [特性](#特性)
- [跨平台使用 🖥️](#跨平台使用-)
- [通过Git协作 👩‍💻🧑‍💻](#通过git协作-)
- [重要链接 📌](#重要链接-)
- [展示 🎥](#展示-)
- [分享评价 📣](#分享评价-)
- [发布到新的包管理器](#发布到新的包管理器)
- [联系方式 🌐](#联系方式-)
- [商标](#商标)
- [贡献 👩‍💻🧑‍💻](#贡献-)
- [作者](#作者)
- [许可证 📄](#许可证-)
## 安装
Bruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) 适用于Mac、Windows 和 Linux 的可执行文件。
Bruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) Mac、Windows 和 Linux 的可执行文件
您也可以通过包管理器如 Homebrew、Chocolatey、Scoop、Snap 和 Apt 安装 Bruno。
@@ -82,15 +58,9 @@ choco install bruno
scoop bucket add extras
scoop install bruno
# 在 Windows 上用 winget 安装
winget install Bruno.Bruno
# 在 Linux 上用 Snap 安装
snap install bruno
# 在 Linux 上用 Flatpak 安装
flatpak install com.usebruno.Bruno
# 在 Linux 上用 Apt 安装
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
@@ -103,50 +73,67 @@ echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebr
sudo apt update && sudo apt install bruno
```
## 特性
### 跨平台使用 🖥️
### 在 Mac 上通过 Homebrew 安装 🖥️
![bruno](../../assets/images/run-anywhere.png) <br /><br />
### 通过Git协作 👩‍💻🧑‍💻
### Collaborate 安装 👩‍💻🧑‍💻
或者任何您选择的版本控制系统
![bruno](../../assets/images/version-control.png) <br /><br />
## 重要链接 📌
### 重要链接 📌
- [我们的愿景](https://github.com/usebruno/bruno/discussions/269)
- [路线图](https://www.usebruno.com/roadmap)
- [路线图](https://github.com/usebruno/bruno/discussions/384)
- [文档](https://docs.usebruno.com)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
- [网站](https://www.usebruno.com)
- [价格](https://www.usebruno.com/pricing)
- [下载](https://www.usebruno.com/downloads)
- [GitHub 赞助](https://github.com/sponsors/helloanoop).
## 展示 🎥
### 展示 🎥
- [Testimonials](https://github.com/usebruno/bruno/discussions/343)
- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
## 分享评价 📣
### 支持 ❤️
如果您喜欢 Bruno 并想支持我们的开源工作,请考虑通过 [GitHub Sponsors](https://github.com/sponsors/helloanoop) 来赞助我们。
### 分享评价 📣
如果 Bruno 在您的工作和团队中帮助了您,请不要忘记在我们的 GitHub 讨论上分享您的 [评价](https://github.com/usebruno/bruno/discussions/343)
## 发布到新的包管理器
### 发布到新的包管理器
如需了解更多信息,请参见 [此处](../publishing/publishing_cn.md) 。
有关更多信息,请参见 [此处](../publishing/publishing_cn.md) 。
## 联系方式 🌐
### 贡献 👩‍💻🧑‍💻
我很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。
即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。
### 作者
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### 联系方式 🌐
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
[Website](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno)
## 商标
### 商标
**名称**
@@ -156,20 +143,6 @@ sudo apt update && sudo apt install bruno
Logo 源自 [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
## 贡献 👩‍💻🧑‍💻
很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。
即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。
## 作者
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
## 许可证 📄
### 许可证 📄
[MIT](../../license.md)

1
package-lock.json generated
View File

@@ -31986,7 +31986,6 @@
"cheerio": "^1.0.0",
"crypto-js": "^4.2.0",
"json-query": "^2.2.2",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nanoid": "3.3.8",

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import debounce from 'lodash/debounce';
import { IconRegex, IconArrowUp, IconArrowDown, IconX, IconLetterCase, IconLetterW } from '@tabler/icons';
import ToolHint from 'components/ToolHint';
import StyledWrapper from './StyledWrapper';
@@ -8,7 +9,7 @@ function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');
}
const CodeMirrorSearch = ({ visible, editor, onClose }) => {
const CustomSearch = ({ visible, editor, onClose }) => {
const [searchText, setSearchText] = useState('');
const [regex, setRegex] = useState(false);
const [caseSensitive, setCaseSensitive] = useState(false);
@@ -198,4 +199,4 @@ const CodeMirrorSearch = ({ visible, editor, onClose }) => {
);
};
export default CodeMirrorSearch;
export default CustomSearch;

View File

@@ -14,7 +14,7 @@ import * as jsonlint from '@prantlf/jsonlint';
import { JSHINT } from 'jshint';
import stripJsonComments from 'strip-json-comments';
import { getAllVariables } from 'utils/collections';
import CodeMirrorSearch from 'components/CodeMirrorSearch';
import CustomSearch from './CustomSearch';
const CodeMirror = require('codemirror');
window.jsonlint = jsonlint;
@@ -267,7 +267,7 @@ export default class CodeEditor extends React.Component {
font={this.props.font}
fontSize={this.props.fontSize}
>
<CodeMirrorSearch
<CustomSearch
visible={this.state.searchBarVisible}
editor={this.editor}
onClose={() => this.setState({ searchBarVisible: false })}

View File

@@ -44,11 +44,7 @@ const CollectionSettings = ({ collection }) => {
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
const presets = get(collection, 'brunoConfig.presets', []);
const hasPresets = presets && presets.requestUrl !== "";
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
const proxyEnabled = proxyConfig.hostname ? true : false;
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
const protobufConfig = get(collection, 'brunoConfig.protobuf', {});
@@ -167,11 +163,10 @@ const CollectionSettings = ({ collection }) => {
</div>
<div className={getTabClassname('presets')} role="tab" onClick={() => setTab('presets')}>
Presets
{hasPresets && <StatusDot />}
</div>
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
Proxy
{Object.keys(proxyConfig).length > 0 && proxyEnabled && <StatusDot />}
{Object.keys(proxyConfig).length > 0 && <StatusDot />}
</div>
<div className={getTabClassname('clientCert')} role="tab" onClick={() => setTab('clientCert')}>
Client Certificates

View File

@@ -17,7 +17,7 @@ import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth';
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 { findItemInCollection, findParentItemInCollection, humanizeRequestAuthMode } from 'utils/collections/index';
const GrantTypeComponentMap = ({ collection, folder }) => {
const dispatch = useDispatch();
@@ -48,7 +48,15 @@ const Auth = ({ collection, folder }) => {
let request = get(folder, 'root.request', {});
const authMode = get(folder, 'root.request.auth.mode');
const getTreePathFromCollectionToFolder = (collection, _folder) => {
let path = [];
let item = findItemInCollection(collection, _folder?.uid);
while (item) {
path.unshift(item);
item = findParentItemInCollection(collection, item?.uid);
}
return path;
};
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
@@ -61,7 +69,7 @@ const Auth = ({ collection, folder }) => {
};
// Get path from collection to current folder
const folderTreePath = getTreePathFromCollectionToItem(collection, folder);
const folderTreePath = getTreePathFromCollectionToFolder(collection, folder);
// Check parent folders to find closest auth configuration
// Skip the last item which is the current folder

View File

@@ -136,7 +136,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
/>
</td>
<td>
<div className="flex items-center" data-testid={`env-var-name-${index}`}>
<div className="flex items-center">
<input
type="text"
autoComplete="off"
@@ -153,7 +153,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
</div>
</td>
<td className="flex flex-row flex-nowrap items-center">
<div className="overflow-hidden grow w-full relative" data-testid={`env-var-value-${index}`}>
<div className="overflow-hidden grow w-full relative">
<MultiLineEditor
theme={storedTheme}
collection={_collection}

View File

@@ -9,8 +9,7 @@ const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
<div className="bruno-modal-header">
{customHeader ? customHeader : <>{title ? <div className="bruno-modal-header-title">{title}</div> : null}</>}
{handleCancel && !hideClose ? (
// TODO: Remove data-test-id and use data-testid instead across the codebase.
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null} data-test-id="modal-close-button" data-testid="modal-close-button">
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null} data-test-id="modal-close-button">
×
</div>
) : null}

View File

@@ -9,7 +9,6 @@ import WsseAuth from './WsseAuth';
import NTLMAuth from './NTLMAuth';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import ApiKeyAuth from './ApiKeyAuth';
import StyledWrapper from './StyledWrapper';
@@ -28,7 +27,6 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
};
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);
@@ -39,7 +37,7 @@ const Auth = ({ item, collection }) => {
// Save function for request level
const save = () => {
return dispatch(saveRequest(item.uid, collection.uid));
return saveRequest(item.uid, collection.uid);
};
const getEffectiveAuthSource = () => {

View File

@@ -8,7 +8,7 @@ import { humanizeRequestBodyMode } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import { updateRequestBody } from 'providers/ReduxStore/slices/collections/index';
import { toastError } from 'utils/common/error';
import { prettifyJSON } from 'utils/common';
import { format, applyEdits } from 'jsonc-parser';
import xmlFormat from 'xml-formatter';
const RequestBodyMode = ({ item, collection }) => {
@@ -39,7 +39,8 @@ const RequestBodyMode = ({ item, collection }) => {
const onPrettify = () => {
if (body?.json && bodyMode === 'json') {
try {
const prettyBodyJson = prettifyJSON(body.json);
const edits = format(body.json, undefined, { tabSize: 2, insertSpaces: true });
const prettyBodyJson = applyEdits(body.json, edits);
dispatch(
updateRequestBody({
content: prettyBodyJson,

View File

@@ -96,17 +96,15 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
const getMethodText = useCallback((item) => {
if (!item) return;
switch (item.type) {
case 'grpc-request':
return 'gRPC';
case 'ws-request':
return 'WS';
case 'graphql-request':
return 'GQL';
default:
return item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
const isGrpc = item.type === 'grpc-request';
const isWS = item.type === 'ws-request';
if (!isWS && !isGrpc) {
return item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
}
if (isGrpc) {
return 'gRPC';
}
return 'WS';
}, [item]);
if (!item) {
@@ -251,28 +249,6 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
} catch (err) {}
}
function handleRevertChanges(event) {
event.stopPropagation();
dropdownTippyRef.current.hide();
if (!currentTabUid) {
return;
}
try {
const item = findItemInCollection(collection, currentTabUid);
if (item.draft) {
dispatch(
deleteRequestDraft({
itemUid: item.uid,
collectionUid: collection.uid
})
);
}
} catch (err) {}
}
function handleCloseOtherTabs(event) {
dropdownTippyRef.current.hide();
@@ -340,13 +316,6 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
>
Clone Request
</button>
<button
className="dropdown-item w-full"
onClick={handleRevertChanges}
disabled={!currentTabItem?.draft}
>
Revert Changes
</button>
<button className="dropdown-item w-full" onClick={(e) => handleCloseTab(e, currentTabUid)}>
Close
</button>

View File

@@ -4,7 +4,7 @@ import { get } from 'lodash';
import find from 'lodash/find';
import { useDispatch, useSelector } from 'react-redux';
import { updateResponsePaneScrollPosition } from 'providers/ReduxStore/slices/tabs';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { Document, Page } from 'react-pdf';
import 'pdfjs-dist/build/pdf.worker';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
@@ -76,8 +76,6 @@ const QueryResultPreview = ({
dispatch(sendRequest(item, collection.uid));
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const onScroll = (event) => {
dispatch(
updateResponsePaneScrollPosition({
@@ -129,7 +127,6 @@ const QueryResultPreview = ({
fontSize={get(preferences, 'font.codeFontSize')}
theme={displayedTheme}
onRun={onRun}
onSave={onSave}
onScroll={onScroll}
value={formattedData}
mode={mode}

View File

@@ -10,6 +10,7 @@ import { findCollectionByItemUid, getGlobalEnvironmentVariables } from 'utils/co
import { cloneDeep } from 'lodash';
import { useMemo } from 'react';
import { generateSnippet } from '../utils/snippet-generator';
const CodeView = ({ language, item }) => {
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
@@ -32,12 +33,7 @@ const CodeView = ({ language, item }) => {
}, [collectionOriginal, globalEnvironments, activeGlobalEnvironmentUid]);
const snippet = useMemo(() => {
return generateSnippet({
language,
item,
collection,
shouldInterpolate: generateCodePrefs.shouldInterpolate
});
return generateSnippet({ language, item, collection, shouldInterpolate: generateCodePrefs.shouldInterpolate });
}, [language, item, collection, generateCodePrefs.shouldInterpolate]);
return (

View File

@@ -40,9 +40,6 @@ const Wrapper = styled.div`
.method-ws {
color: ${(props) => props.theme.request.ws};
}
.method-graphql {
color: ${(props) => props.theme.request.gql};
}
`;
export default Wrapper;

View File

@@ -4,20 +4,18 @@ import StyledWrapper from './StyledWrapper';
const getMethodFlags = (item) => ({
isGrpc: item.type === 'grpc-request',
isWS: item.type === 'ws-request',
isGraphQL: item.type === 'graphql-request'
isWS: item.type === 'ws-request'
});
const getMethodText = (item, { isGrpc, isWS, isGraphQL }) => {
const getMethodText = (item, { isGrpc, isWS }) => {
if (isGrpc) return 'grpc';
if (isWS) return 'ws';
if (isGraphQL) return 'gql';
return item.request.method.length > 5
? item.request.method.substring(0, 3)
: item.request.method;
};
const getClassname = (method = '', { isGrpc, isWS, isGraphQL }) => {
const getClassname = (method = '', { isGrpc, isWS }) => {
method = method.toLocaleLowerCase();
return classnames('mr-1', {
'method-get': method === 'get',
@@ -28,8 +26,7 @@ const getClassname = (method = '', { isGrpc, isWS, isGraphQL }) => {
'method-head': method === 'head',
'method-options': method === 'options',
'method-grpc': isGrpc,
'method-ws': isWS,
'method-graphql': isGraphQL
'method-ws': isWS
});
};

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import get from 'lodash/get';
import filter from 'lodash/filter';
import { Inspector, chromeDark, chromeLight } from 'react-inspector';
import { Inspector } from 'react-inspector';
import { useTheme } from 'providers/Theme';
import { findEnvironmentInCollection, maskInputValue } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
@@ -84,12 +84,9 @@ const RuntimeVariables = ({ collection, theme }) => {
};
const VariablesEditor = ({ collection }) => {
const { displayedTheme, theme } = useTheme();
const { storedTheme } = useTheme();
const reactInspectorTheme
= displayedTheme === 'light'
? { ...chromeLight, OBJECT_VALUE_STRING_COLOR: theme.variables.runtime.color }
: { ...chromeDark, OBJECT_VALUE_STRING_COLOR: theme.variables.runtime.color };
const reactInspectorTheme = storedTheme === 'light' ? 'chromeLight' : 'chromeDark';
return (
<StyledWrapper className="px-4 py-4 overflow-auto">

View File

@@ -1,7 +1,7 @@
import { createSlice } from '@reduxjs/toolkit';
import { uuid } from 'utils/common/index';
import { environmentSchema } from '@usebruno/schema';
import { cloneDeep, has } from 'lodash';
import { cloneDeep } from 'lodash';
const initialState = {
globalEnvironments: [],
@@ -195,15 +195,11 @@ export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) =>
}
let variables = cloneDeep(environment?.variables);
console.log('globalEnvironmentVariables', globalEnvironmentVariables);
// "globalEnvironmentVariables" will include only the enabled variables and newly added variables created using the script.
// Update the value of each variable if it's present in "globalEnvironmentVariables", otherwise keep the existing value.
// update existing values
variables = variables?.map?.(variable => ({
...variable,
value: has(globalEnvironmentVariables, variable?.name)
? globalEnvironmentVariables[variable?.name]
: variable?.value
value: globalEnvironmentVariables?.[variable?.name]
}));
// add new env values

View File

@@ -32,10 +32,6 @@ const darkTheme = {
name: {
color: '#569cd6'
},
runtime: {
color: 'rgb(255, 255, 255)'
}
},
@@ -107,8 +103,7 @@ const darkTheme = {
head: '#d69956'
},
grpc: '#6366f1',
ws: '#f59e0b',
gql: '#e535ab'
ws: '#f59e0b'
},
requestTabPanel: {

View File

@@ -36,10 +36,6 @@ const lightTheme = {
name: {
color: '#546de5'
},
runtime: {
color: 'rgb(0, 0, 0)'
}
},
@@ -107,8 +103,7 @@ const lightTheme = {
head: '#ca7811'
},
grpc: '#6366f1',
ws: '#f59e0b',
gql: '#e535ab'
ws: '#f59e0b'
},
requestTabPanel: {

View File

@@ -33,22 +33,6 @@ export const getAuthHeaders = (collectionRootAuth, requestAuth) => {
value: `Bearer ${get(auth, 'bearer.token', '')}`
}
];
case 'apikey':
const apiKeyAuth = get(auth, 'apikey', {});
const key = get(apiKeyAuth, 'key', '');
const value = get(apiKeyAuth, 'value', '');
const placement = get(apiKeyAuth, 'placement', 'header');
if (placement === 'header') {
return [
{
enabled: true,
name: key,
value: value
}
];
}
return [];
default:
return [];
}

View File

@@ -44,25 +44,13 @@ const createHeaders = (request, headers) => {
return enabledHeaders;
};
const createQuery = (queryParams = [], request) => {
const params = queryParams
const createQuery = (queryParams = []) => {
return queryParams
.filter((param) => param.enabled && param.type === 'query')
.map((param) => ({
name: param.name,
value: param.value
}));
if (request?.auth?.mode === 'apikey' &&
request?.auth?.apikey?.placement === 'queryparams' &&
request?.auth?.apikey?.key &&
request?.auth?.apikey?.value) {
params.push({
name: request.auth.apikey.key,
value: request.auth.apikey.value
});
}
return params;
};
const createPostData = (body) => {
@@ -131,20 +119,13 @@ const createPostData = (body) => {
};
export const buildHarRequest = ({ request, headers }) => {
// NOTE:
// This is just a safety check.
// The interpolateUrlPathParams method validates the url, but it does not throw
if (!URL.canParse(request.url)) {
throw new Error('invalid request url');
}
return {
method: request.method,
url: request.url,
url: encodeURI(request.url),
httpVersion: 'HTTP/1.1',
cookies: [],
headers: createHeaders(request, headers),
queryString: createQuery(request.params, request),
queryString: createQuery(request.params),
postData: createPostData(request.body),
headersSize: 0,
bodySize: 0,

View File

@@ -93,9 +93,6 @@ const STATIC_API_HINTS = {
'bru.cookies.jar().clear(callback)',
'bru.cookies.jar().deleteCookies(url, callback)',
'bru.cookies.jar().deleteCookie(url, name, callback)',
'bru.utils',
'bru.utils.minifyJson(json)',
'bru.utils.minifyXml(xml)'
]
};

View File

@@ -54,27 +54,10 @@ export const safeStringifyJSON = (obj, indent = false) => {
export const prettifyJSON = (obj, spaces = 2) => {
try {
const text = obj.replace(/\\"/g, '"').replace(/\\'/g, "'");
const formatted = obj.replace(/\\"/g, '"').replace(/\\'/g, "'");
const edits = format(formatted, undefined, { tabSize: spaces, insertSpaces: true });
const placeholders = [];
const modifiedJson = text.replace(/"[^"]*?"|{{[^{}]+}}/g, (match) => {
if (match.startsWith('{{')) {
const placeholder = `__BRUNO_VAR_PLACEHOLDER_${placeholders.length}__`;
placeholders.push(match);
return `"${placeholder}"`; // Wrap bare variable in quotes to make it a valid JSON string
}
return match;
});
const edits = format(modifiedJson, undefined, { tabSize: spaces, insertSpaces: true });
let result = applyEdits(modifiedJson, edits);
for (let i = 0; i < placeholders.length; i++) {
const placeholder = `__BRUNO_VAR_PLACEHOLDER_${i}__`;
result = result.replace(`"${placeholder}"`, placeholders[i]);
}
return result;
return applyEdits(formatted, edits);
} catch (e) {
return obj;
}

View File

@@ -1,14 +1,6 @@
const { describe, it, expect } = require('@jest/globals');
import {
normalizeFileName,
startsWith,
humanizeDate,
relativeDate,
getContentType,
formatSize,
prettifyJSON
} from './index';
import { normalizeFileName, startsWith, humanizeDate, relativeDate, getContentType, formatSize } from './index';
describe('common utils', () => {
describe('normalizeFileName', () => {
@@ -192,30 +184,4 @@ describe('common utils', () => {
expect(formatSize(NaN)).toBe('0B');
});
});
describe('prettifyJSON', () => {
it('should prettify a standard JSON string', () => {
const input = '{"key":"value","number":123}';
const expected = '{\n "key": "value",\n "number": 123\n}';
expect(prettifyJSON(input)).toBe(expected);
});
it('should handle JSON with a Bruno variable as a value', () => {
const input = '{"id":{{request_id}}}';
const expected = '{\n "id": {{request_id}}\n}';
expect(prettifyJSON(input)).toBe(expected);
});
it('should handle JSON with a Bruno variable inside a string value', () => {
const input = '{"url":"https://example.com/{{path}}"}';
const expected = '{\n "url": "https://example.com/{{path}}"\n}';
expect(prettifyJSON(input)).toBe(expected);
});
it('should return the original string for invalid JSON', () => {
const input = '{"key":"value",';
const expected = '{\n "key": "value",';
expect(prettifyJSON(input)).toBe(expected);
});
});
});

View File

@@ -167,75 +167,6 @@ describe('Url Utils - parsePathParams', () => {
expect(params).toEqual([]);
});
});
describe('Url Utils - URN parsing', () => {
it('should handle basic URN segments correctly', () => {
// Test case from issue #5817 - Don't treat URN segments as path parameters
const params = parsePathParams('https://example.com/urn:ard:show:3479462da794e97');
expect(params).toEqual([]);
// Test case for path parameter that starts with urn:
const params2 = parsePathParams('https://example.com/:urn_type');
expect(params2).toEqual([{ name: 'urn_type', value: '' }]);
});
it('should handle URNs with special characters', () => {
const params = parsePathParams('https://example.com/urn:isbn:0-330.12345-X');
expect(params).toEqual([]);
// URN with percent-encoded characters
const params2 = parsePathParams('https://example.com/urn:uuid:6e8bc430%2D9c3a-11d9-9669-0800200c9a66');
expect(params).toEqual([]);
});
it('should handle mixed URN and path parameter scenarios', () => {
// URN followed by path parameter
const params = parsePathParams('https://example.com/urn:nbn:de:bvb/123/:section');
expect(params).toEqual([{ name: 'section', value: '' }]);
// Path parameter followed by URN
const params2 = parsePathParams('https://example.com/:type/urn:isbn:123');
expect(params2).toEqual([{ name: 'type', value: '' }]);
// URN-like path parameter (not a real URN)
const params3 = parsePathParams('https://example.com/:urn:type');
expect(params3).toEqual([{ name: 'urn:type', value: '' }]);
});
it('should handle edge cases with URN-like patterns', () => {
// URN with uppercase (should be case-insensitive)
const params = parsePathParams('https://example.com/URN:isbn:123');
expect(params).toEqual([]);
// Path that looks like URN but isn't
const params2 = parsePathParams('https://example.com/noturn:something:here');
expect(params2).toEqual([]);
// Multiple colons in path parameter
const params3 = parsePathParams('https://example.com/:urn:isbn:type');
expect(params3).toEqual([{ name: 'urn:isbn:type', value: '' }]);
});
it('should handle URNs in complex URLs', () => {
// URN with query parameters
const params = parsePathParams('https://example.com/urn:isbn:123?format=:format');
expect(params).toEqual([]);
// Multiple URNs and path parameters
const params2 = parsePathParams('https://example.com/:category/urn:isbn:123/:subcategory/urn:issn:456');
expect(params2).toEqual([
{ name: 'category', value: '' },
{ name: 'subcategory', value: '' }
]);
// URN with fragment
const params3 = parsePathParams('https://example.com/urn:nbn:de:bvb:123#:section');
expect(params3).toEqual([]);
});
});
describe('Url Utils - OData parameters', () => {
it('should handle OData parameters with escaped quotes', () => {
const params = parsePathParams('https://example.com/odata/Products(\'ABC\'\'123\')');
expect(params).toEqual([]);

View File

@@ -1,6 +1,5 @@
import { describe, it, expect } from '@jest/globals';
import postmanToBruno from '../../../src/postman/postman-to-bruno';
import { invalidVariableCharacterRegex } from '../../../src/constants';
describe('postman-collection', () => {
it('should correctly import a valid Postman collection file', async () => {
@@ -8,25 +7,6 @@ describe('postman-collection', () => {
expect(brunoCollection).toMatchObject(expectedOutput);
});
it('should replace invalid variable characters with underscores', () => {
const variables = [
{ key: 'validKey', value: 'value1' },
{ key: 'invalid key', value: 'value2' },
{ key: 'another@invalid#key$', value: 'value3' }
];
const processedVariables = variables.map((v) => ({
name: v.key.replace(invalidVariableCharacterRegex, '_'),
value: v.value
}));
expect(processedVariables).toEqual([
{ name: 'validKey', value: 'value1' },
{ name: 'invalid_key', value: 'value2' },
{ name: 'another_invalid_key_', value: 'value3' }
]);
});
it('should handle falsy values in collection variables', async () => {
const collectionWithFalsyVars = {
"info": {

View File

@@ -277,7 +277,6 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col
const runtimeVars = collection.runtimeVariables;
// Precedence: runtimeVars > requestVariables > folderVars > envVars > collectionVariables > globalEnvironmentVars
const processEnvVars = getProcessEnvVars(collection.uid);
const resolvedVars = merge(
{},
globalEnvironmentVars,
@@ -285,14 +284,7 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col
envVars,
folderVars,
requestVariables,
runtimeVars,
{
process: {
env: {
...processEnvVars
}
}
}
runtimeVars
);
const collectionRoot = get(collection, 'root', {});
@@ -309,6 +301,7 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col
}
const collectionPath = collection.pathname;
const processEnvVars = getProcessEnvVars(collection.uid);
const axiosInstance = await configureRequest(
collection.uid,

View File

@@ -63,32 +63,4 @@ describe('prepareGqlIntrospectionRequest', () => {
expect(result.headers['Content-Type']).toBe('application/json');
});
it('should handle process.env variables in endpoint URL', () => {
const setup = createBasicSetup();
setup.endpoint = 'https://{{process.env.API_HOST}}/graphql';
const vars = {
process: {
env: {
API_HOST: 'api.example.com'
}
}
};
const result = prepareGqlIntrospectionRequest(setup.endpoint, vars, setup.request, setup.collectionRoot);
expect(result.url).toBe('https://api.example.com/graphql');
expect(result.method).toBe('POST');
});
it('should handle missing process.env variables gracefully', () => {
const setup = createBasicSetup();
setup.request.headers = [
{ name: 'X-API-Key', value: '{{process.env.MISSING_VAR}}', enabled: true }
];
const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot);
expect(result.headers['X-API-Key']).toBe('{{process.env.MISSING_VAR}}');
});
});

View File

@@ -27,7 +27,6 @@
"cheerio": "^1.0.0",
"crypto-js": "^4.2.0",
"json-query": "^2.2.2",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nanoid": "3.3.8",

View File

@@ -1,5 +1,4 @@
const { cloneDeep } = require('lodash');
const xmlFormat = require('xml-formatter');
const { interpolate: _interpolate } = require('@usebruno/common');
const { sendRequest } = require('@usebruno/requests').scripting;
const { jar: createCookieJar } = require('@usebruno/requests').cookies;
@@ -75,50 +74,6 @@ class Bru {
this.nextRequest = nextRequest;
}
};
this.utils = {
minifyJson: (json) => {
if (json === null || json === undefined) {
throw new Error('Failed to minify');
}
if (typeof json === 'object') {
try {
return JSON.stringify(json);
} catch (err) {
throw new Error(`Failed to minify: ${err?.message || err}`);
}
}
if (typeof json === 'string') {
const trimmed = json.trim();
if (trimmed === '') return trimmed;
try {
return JSON.stringify(JSON.parse(trimmed));
} catch (err) {
throw new Error(`Failed to minify: ${err?.message || err}`);
}
}
throw new TypeError('minifyJson expects a string or object');
},
minifyXml: (xml) => {
if (xml === null || xml === undefined) {
throw new Error('Failed to minify');
}
if (typeof xml === 'string') {
try {
return xmlFormat(xml, { collapseContent: false, indentation: '', lineSeparator: '' });
} catch (err) {
throw new Error(`Failed to minify: ${err?.message || err}`);
}
}
throw new TypeError('minifyXml expects a string');
}
};
}
interpolate = (strOrObj) => {

View File

@@ -33,7 +33,6 @@ const NodeVault = require('node-vault');
const xml2js = require('xml2js');
const cheerio = require('cheerio');
const tv4 = require('tv4');
const jsonwebtoken = require('jsonwebtoken');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
class ScriptRuntime {
@@ -186,7 +185,6 @@ class ScriptRuntime {
'node-fetch': fetch,
'crypto-js': CryptoJS,
xml2js: xml2js,
jsonwebtoken,
cheerio,
tv4,
...whitelistedModules,
@@ -356,7 +354,6 @@ class ScriptRuntime {
'node-fetch': fetch,
'crypto-js': CryptoJS,
'xml2js': xml2js,
jsonwebtoken,
cheerio,
tv4,
...whitelistedModules,

View File

@@ -35,7 +35,6 @@ const NodeVault = require('node-vault');
const xml2js = require('xml2js');
const cheerio = require('cheerio');
const tv4 = require('tv4');
const jsonwebtoken = require('jsonwebtoken');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
class TestRuntime {
@@ -104,8 +103,7 @@ class TestRuntime {
res,
expect: chai.expect,
assert: chai.assert,
__brunoTestResults: __brunoTestResults,
jwt: jsonwebtoken
__brunoTestResults: __brunoTestResults
};
if (onConsoleLog && typeof onConsoleLog === 'function') {
@@ -178,7 +176,6 @@ class TestRuntime {
'xml2js': xml2js,
cheerio,
tv4,
'jsonwebtoken': jsonwebtoken,
...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined,
'node-vault': NodeVault

View File

@@ -87,10 +87,9 @@ const addBrunoRequestShimToContext = (vm, req) => {
vm.setProp(reqObject, 'setHeader', setHeader);
setHeader.dispose();
let getBody = vm.newFunction('getBody', function (options) {
return marshallToVm(req.getBody(vm.dump(options)), vm);
let getBody = vm.newFunction('getBody', function () {
return marshallToVm(req.getBody(), vm);
});
vm.setProp(reqObject, 'getBody', getBody);
getBody.dispose();

View File

@@ -2,14 +2,12 @@ const addAxiosShimToContext = require('./axios');
const addNanoidShimToContext = require('./nanoid');
const addPathShimToContext = require('./path');
const addUuidShimToContext = require('./uuid');
const addJwtShimToContext = require('./jwt');
const addLibraryShimsToContext = async (vm) => {
await addNanoidShimToContext(vm);
await addAxiosShimToContext(vm);
await addUuidShimToContext(vm);
await addPathShimToContext(vm);
await addJwtShimToContext(vm);
};
module.exports = addLibraryShimsToContext;

View File

@@ -1,181 +0,0 @@
const jwt = require('jsonwebtoken');
const { marshallToVm, invokeFunction } = require('../../utils');
const addJwtShimToContext = async (vm) => {
// --- sign ---
const _jwtSign = vm.newFunction('sign', function (payload, secret, options, callback) {
const nativePayload = vm.dump(payload);
const nativeSecret = vm.dump(secret);
let nativeOptions;
let callbackHandle = callback;
const optionsType = options === undefined ? 'undefined' : vm.typeof(options);
if (optionsType === 'function') {
callbackHandle = options;
nativeOptions = undefined;
} else if (optionsType === 'object' && options !== null) {
nativeOptions = vm.dump(options);
}
// If a callback is provided
if (callbackHandle && vm.typeof(callbackHandle) === 'function') {
let tokenResult;
let hostError;
try {
tokenResult = nativeOptions
? jwt.sign(nativePayload, nativeSecret, nativeOptions)
: jwt.sign(nativePayload, nativeSecret);
} catch (err) {
hostError = err;
}
try {
if (hostError) {
const errVm = vm.newError(hostError.message || String(hostError));
invokeFunction(vm, callbackHandle, [errVm, vm.undefined])
.catch((e) => {
console.warn('[JWT SHIM][sign.cb] callback invocation error:', e);
})
.finally(() => {
errVm.dispose();
callbackHandle.dispose();
});
} else {
const tokenVm = marshallToVm(String(tokenResult), vm);
invokeFunction(vm, callbackHandle, [vm.null, tokenVm])
.catch((e) => {
console.warn('[JWT SHIM][sign.cb] callback invocation error:', e);
})
.finally(() => {
tokenVm.dispose();
callbackHandle.dispose();
});
}
} catch (e) {
console.warn('[JWT SHIM][sign.cb] unexpected error:', e);
callbackHandle.dispose();
}
return vm.undefined;
}
try {
const token = nativeOptions
? jwt.sign(nativePayload, nativeSecret, nativeOptions)
: jwt.sign(nativePayload, nativeSecret);
return marshallToVm(token, vm);
} catch (err) {
throw vm.newError(err.message || String(err));
}
});
vm.setProp(vm.global, '__bruno__jwt__sign', _jwtSign);
_jwtSign.dispose();
// --- verify ---
const _jwtVerify = vm.newFunction('verify', function (token, secret, options, callback) {
const nativeToken = vm.dump(token);
const nativeSecret = vm.dump(secret);
let nativeOptions;
let actualCallback = callback;
const optionsType = options === undefined ? 'undefined' : vm.typeof(options);
if (optionsType === 'function') {
actualCallback = options;
nativeOptions = undefined;
} else if (optionsType === 'object' && options !== null) {
nativeOptions = vm.dump(options);
}
if (actualCallback && vm.typeof(actualCallback) === 'function') {
let decodedResult;
let hostError;
try {
decodedResult = nativeOptions
? jwt.verify(nativeToken, nativeSecret, nativeOptions)
: jwt.verify(nativeToken, nativeSecret);
} catch (err) {
hostError = err;
}
try {
if (hostError) {
const vmErr = vm.newError(hostError.message || String(hostError));
invokeFunction(vm, actualCallback, [vmErr, vm.undefined])
.catch((e) => {
console.warn('[JWT SHIM][verify.cb] callback invocation error:', e);
})
.finally(() => {
vmErr.dispose();
actualCallback.dispose();
});
} else {
const vmNull = vm.null;
const vmDecoded = marshallToVm(decodedResult, vm);
invokeFunction(vm, actualCallback, [vmNull, vmDecoded])
.catch((e) => {
console.warn('[JWT SHIM][verify.cb] callback invocation error:', e);
})
.finally(() => {
vmDecoded.dispose();
actualCallback.dispose();
});
}
} catch (e) {
console.warn('[JWT SHIM][verify.cb] unexpected error:', e);
actualCallback.dispose();
}
return vm.undefined;
}
try {
const decoded = nativeOptions
? jwt.verify(nativeToken, nativeSecret, nativeOptions)
: jwt.verify(nativeToken, nativeSecret);
return marshallToVm(decoded, vm);
} catch (err) {
throw vm.newError(err.message || String(err));
}
});
vm.setProp(vm.global, '__bruno__jwt__verify', _jwtVerify);
_jwtVerify.dispose();
// --- decode ---
const _jwtDecode = vm.newFunction('decode', function (token, options) {
const nativeToken = vm.dump(token);
let nativeOptions;
const optionsType = options === undefined ? 'undefined' : vm.typeof(options);
if (optionsType === 'object' && options !== null) {
nativeOptions = vm.dump(options);
}
try {
const decoded = nativeOptions
? jwt.decode(nativeToken, nativeOptions)
: jwt.decode(nativeToken);
return marshallToVm(decoded, vm);
} catch (err) {
throw vm.newError(err.message || String(err));
}
});
vm.setProp(vm.global, '__bruno__jwt__decode', _jwtDecode);
_jwtDecode.dispose();
vm.evalCode(`
globalThis.jwt = {};
globalThis.jwt.sign = globalThis.__bruno__jwt__sign;
globalThis.jwt.verify = globalThis.__bruno__jwt__verify;
globalThis.jwt.decode = globalThis.__bruno__jwt__decode;
globalThis.requireObject = {
...globalThis.requireObject,
'jsonwebtoken': globalThis.jwt,
};
`);
};
module.exports = addJwtShimToContext;

View File

@@ -30,54 +30,6 @@ const marshallToVm = (value, vm) => {
}
};
/**
* Invokes a QuickJS function handle.
* - Returns a Promise
*
* @param {Object} vm - QuickJS VM instance
* @param {QuickJSHandle} quickFn - A QuickJS function handle
* @param {Array} args - Arguments to pass to the function
* @returns {Promise<any>} - The result as a Promise
*/
async function invokeFunction(vm, quickFn, args = []) {
if (vm.typeof(quickFn) !== 'function') {
throw new TypeError('Target is not a QuickJS function');
}
const result = vm.callFunction(quickFn, vm.global, ...args);
if (result.error) {
const error = vm.dump(result.error);
result.error.dispose();
throw error;
}
// Check if the result is a QuickJS Promise handle (async functions)
if (vm.typeof(result.value) === 'object' && result.value.constructor && vm.typeof(result.value.constructor) === 'function') {
try {
const promiseHandle = vm.unwrapResult(result);
const resolvedResult = await vm.resolvePromise(promiseHandle);
promiseHandle.dispose();
const resolvedHandle = vm.unwrapResult(resolvedResult);
const value = vm.dump(resolvedHandle);
resolvedHandle.dispose();
return Promise.resolve(value);
} catch (promiseError) {
// If it's not a valid Promise, throw an error
result.value.dispose();
throw new Error(`Invalid Promise handle: ${promiseError.message}`);
}
}
const value = vm.dump(result.value);
result.value.dispose();
return (value && typeof value.then === 'function')
? value
: Promise.resolve(value);
}
module.exports = {
marshallToVm,
invokeFunction
marshallToVm
};

View File

@@ -92,9 +92,6 @@ snap install bruno
# On Linux via Flatpak
flatpak install com.usebruno.Bruno
# On Arch Linux via AUR
yay -S bruno
# On Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl

View File

@@ -1,9 +0,0 @@
{
"version": "1",
"name": "Global Environment Update",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View File

@@ -1,12 +0,0 @@
meta {
name: Global Environment Update
}
auth {
mode: none
}
script:pre-request {
//create a new global env variable.
bru.setGlobalEnvVar('newEnv', "newEnvValue");
}

View File

@@ -1,24 +0,0 @@
meta {
name: Test Request
type: http
seq: 1
}
get {
url: {{baseUrl}}/users
body: json
auth: inherit
}
script:pre-request {
//update already existing enabled env variable
bru.setGlobalEnvVar("existingEnvEnabled", "newExistingEnvEnabledValue");
//update already existing disabled env variable
bru.setGlobalEnvVar("existingEnvDisabled", "newExistingEnvDisabledValue");
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -1,62 +0,0 @@
import { test, expect } from '../../../playwright';
import { closeAllCollections } from '../../utils/page';
test.describe('Global Environment Variable Update via Script', () => {
test.afterEach(async ({ pageWithUserData: page }) => {
// cleanup: close all collections
await closeAllCollections(page);
});
test('should update global environment values via script and verify the changes', async ({
pageWithUserData: page
}) => {
await test.step('Open the collection from sidebar', async () => {
await page.locator('#sidebar-collection-name').filter({ hasText: 'Global Environment Update' }).click();
});
await test.step('Open the test request that has a pre-request script', async () => {
await page.locator('.collection-name', { hasText: 'Global Environment Update' }).click();
await page.locator('.collection-item-name', { hasText: 'Test Request' }).click();
});
await test.step('Run the request', async () => {
await page.getByTestId('send-arrow-icon').click();
});
await test.step('Open the Global Environment Config modal', async () => {
await page.getByTestId('environment-selector-trigger').click();
await page.getByTestId('env-tab-global').click();
await page.getByText('Configure', { exact: true }).click();
});
const globalEnvModal = page.locator('.bruno-modal').filter({ hasText: 'Global Environments' });
await test.step('Verify that the value of "existingEnvEnabled" is updated by the pre-request script', async () => {
const updatedExistingEnvEnabledInputDiv = await globalEnvModal.getByTestId('env-var-value-1');
const updatedExistingEnvEnabledValue = await updatedExistingEnvEnabledInputDiv.locator('.CodeMirror-line').textContent();
await expect(updatedExistingEnvEnabledValue).toContain('newExistingEnvEnabledValue');
});
await test.step('Verify that the value of "existingEnvDisabled" is updated by the pre-request script', async () => {
const updatedExistingEnvDisabledInputDiv = await globalEnvModal.getByTestId('env-var-value-2');
const updatedExistingEnvDisabledValue = await updatedExistingEnvDisabledInputDiv.locator('.CodeMirror-line').textContent();
await expect(updatedExistingEnvDisabledValue).toContain('newExistingEnvDisabledValue');
});
await test.step('Verify that a new env variable "newEnv" is added by the pre-request script to the global environment', async () => {
const newEnvInputDiv = await globalEnvModal.getByTestId('env-var-value-3');
const newEnvValue = await newEnvInputDiv.locator('.CodeMirror-line').textContent();
await expect(newEnvValue).toContain('newEnvValue');
});
await test.step('Verify that the value of "baseUrl" is unchanged.', async () => {
const currentBaseUrlInputDiv = await globalEnvModal.getByTestId('env-var-value-0');
const currentBaseUrlValue = await currentBaseUrlInputDiv.locator('.CodeMirror-line').textContent();
await expect(currentBaseUrlValue).toContain('https://echo.usebruno.com');
});
await test.step('Close the global environment config modal.', async () => {
await page.getByTestId('modal-close-button').click();
});
});
});

View File

@@ -1,10 +0,0 @@
{
"collections": [
{
"path": "{{projectRoot}}/tests/environments/update-global-environment-via-script/fixtures/collection",
"securityConfig": {
"jsSandboxMode": "safe"
}
}
]
}

View File

@@ -1,35 +0,0 @@
{
"environments": [
{
"uid": "RrPsTcwRnHMv3yljQO3ex",
"name": "global",
"variables": [
{
"uid": "VXKOZdkYw0DyI4mlhn6Wr",
"name": "baseUrl",
"value": "https://echo.usebruno.com",
"type": "text",
"secret": false,
"enabled": true
},
{
"uid": "NTwrSscXsaeh4uee6ocJN",
"name": "existingEnvEnabled",
"value": "existingEnvEnabledValue",
"type": "text",
"secret": false,
"enabled": true
},
{
"uid": "PCsUccFm4pktVowXEKRvw",
"name": "existingEnvDisabled",
"value": "existingEnvDisabledValue",
"type": "text",
"secret": false,
"enabled": false
}
]
}
],
"activeGlobalEnvironmentUid": "RrPsTcwRnHMv3yljQO3ex"
}

View File

@@ -1,6 +0,0 @@
{
"maximized": true,
"lastOpenedCollections": [
"{{projectRoot}}/tests/environments/update-global-environment-via-script/fixtures/collection"
]
}

View File

@@ -22,6 +22,4 @@ test('Should verify all support links with correct URL in preference > Support t
const locator_documentation = page.getByRole('link', { name: 'Documentation', exact: true });
expect(await locator_documentation.getAttribute('href')).toEqual('https://docs.usebruno.com');
await page.locator('[data-test-id="modal-close-button"]').click();
});
});

View File

@@ -1,100 +0,0 @@
import { test, expect } from '../../../playwright';
import { closeAllCollections } from '../../utils/page';
test.describe('Code Generation URL Encoding', () => {
test.afterEach(async ({ pageWithUserData: page }) => {
try {
const modalCloseButton = page.locator('[data-test-id="modal-close-button"]');
if (await modalCloseButton.isVisible()) {
await modalCloseButton.click();
await modalCloseButton.waitFor({ state: 'hidden' });
}
} catch (e) {}
await closeAllCollections(page);
});
test('Should generate code with proper URL encoding for unencoded input', async ({
pageWithUserData: page,
createTmpDir
}) => {
await page.locator('.dropdown-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
await page.getByLabel('Name').fill('unencoded-test-collection');
await page.getByLabel('Location').fill(await createTmpDir('unencoded-test-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'unencoded-test-collection' })).toBeVisible();
await page.locator('#sidebar-collection-name').filter({ hasText: 'unencoded-test-collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
await page.locator('#create-new-tab').getByRole('img').click();
await page.getByPlaceholder('Request Name').fill('unencoded-request');
await page.locator('#new-request-url .CodeMirror').click();
await page.locator('textarea').fill('http://base.source?name=John Doe');
await page.getByRole('button', { name: 'Create' }).click();
await expect(page.locator('.collection-item-name').filter({ hasText: 'unencoded-request' })).toBeVisible();
await page.locator('.collection-item-name').filter({ hasText: 'unencoded-request' }).click();
await page.locator('#send-request .infotip').first().click();
await expect(page.getByRole('dialog')).toBeVisible();
await expect(page.getByRole('dialog').locator('.bruno-modal-header-title')).toContainText('Generate Code');
const codeEditor = page.locator('.editor-content .CodeMirror').first();
await expect(codeEditor).toBeVisible();
const generatedCode = await codeEditor.textContent();
expect(generatedCode).toContain('http://base.source/?name=John%20Doe');
await page.locator('[data-test-id="modal-close-button"]').click();
await page.locator('[data-test-id="modal-close-button"]').waitFor({ state: 'hidden' });
});
test('Should generate code with proper URL encoding for encoded input', async ({
pageWithUserData: page,
createTmpDir
}) => {
await page.locator('.dropdown-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
await page.getByLabel('Name').fill('encoded-test-collection');
await page.getByLabel('Location').fill(await createTmpDir('encoded-test-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'encoded-test-collection' })).toBeVisible();
await page.locator('#sidebar-collection-name').filter({ hasText: 'encoded-test-collection' }).click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
await page.locator('#create-new-tab').getByRole('img').click();
await page.getByPlaceholder('Request Name').fill('encoded-request');
await page.locator('#new-request-url .CodeMirror').click();
await page.locator('textarea').fill('http://base.source?name=John%20Doe');
await page.getByRole('button', { name: 'Create' }).click();
await expect(page.locator('.collection-item-name').filter({ hasText: 'encoded-request' })).toBeVisible();
await page.locator('.collection-item-name').filter({ hasText: 'encoded-request' }).click();
await page.locator('#send-request .infotip').first().click();
await expect(page.getByRole('dialog')).toBeVisible();
await expect(page.getByRole('dialog').locator('.bruno-modal-header-title')).toContainText('Generate Code');
const codeEditor = page.locator('.editor-content .CodeMirror').first();
await expect(codeEditor).toBeVisible();
const generatedCode = await codeEditor.textContent();
expect(generatedCode).toContain('http://base.source/?name=John%20Doe');
await page.locator('[data-test-id="modal-close-button"]').click();
await page.locator('[data-test-id="modal-close-button"]').waitFor({ state: 'hidden' });
});
});

View File

@@ -1,9 +0,0 @@
{
"version": "1",
"name": "jsonwebtoken",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View File

@@ -1,66 +0,0 @@
meta {
name: decode
type: http
seq: 1
}
post {
url: {{host}}/api/echo
body: none
auth: inherit
}
script:pre-request {
const jwt = require('jsonwebtoken');
const testPayload = {
userId: 456,
username: 'decodeuser',
role: 'user',
iat: Math.floor(Date.now() / 1000)
};
const secret = bru.getEnvVar('secret') || 'test-secret-key';
const testToken = jwt.sign(testPayload, secret, { algorithm: 'HS256', expiresIn: '1h' });
try {
console.log('Testing JWT decoding...');
console.log('Test token:', testToken);
const decoded = jwt.decode(testToken);
bru.setEnvVar('decoded_payload', JSON.stringify(decoded));
} catch (error) {
console.error('JWT decoding failed:', error.message);
throw error;
}
}
tests {
test("Decoded payload should exist", function() {
const decodedPayload = bru.getEnvVar('decoded_payload');
expect(decodedPayload).to.exist;
});
test("Decoded payload should contain correct user data", function() {
const decodedPayload = JSON.parse(bru.getEnvVar('decoded_payload'));
expect(decodedPayload.userId).to.equal(456);
expect(decodedPayload.username).to.equal('decodeuser');
expect(decodedPayload.role).to.equal('user');
});
test("Decoded payload should have timestamp fields", function() {
const decodedPayload = JSON.parse(bru.getEnvVar('decoded_payload'));
expect(decodedPayload.iat).to.exist;
expect(decodedPayload.exp).to.exist;
expect(typeof decodedPayload.iat).to.equal('number');
expect(typeof decodedPayload.exp).to.equal('number');
});
}
settings {
encodeUrl: true
}

View File

@@ -1,8 +0,0 @@
meta {
name: decode
seq: 3
}
auth {
mode: inherit
}

View File

@@ -1,4 +0,0 @@
vars {
host: http://httpfaker.org
secret: my-secret-key
}

View File

@@ -1,8 +0,0 @@
meta {
name: sign
seq: 1
}
auth {
mode: inherit
}

View File

@@ -1,74 +0,0 @@
meta {
name: sign with callback err
type: http
seq: 2
}
post {
url: {{host}}/api/echo
body: none
auth: inherit
}
tests {
const jwt = require('jsonwebtoken');
const HS_SECRET = 'supersecret';
/**
* Helper that calls jwt.sign **with a callback** and resolves/rejects
* based on the callback's (err, token) — so tests can `await` it.
*/
function signViaCallback(payload, secret, options = {}) {
return new Promise((resolve, reject) => {
jwt.sign(payload, secret, options, (err, token) => {
if (err) return reject(err);
resolve(token);
});
});
}
/* ============================================================
ERROR TESTS — jwt.sign should call callback with `err`
============================================================ */
test('ERROR (callback) — missing secret for HS256', async function () {
try {
await signViaCallback({ sub: 'no_secret' }, undefined, { algorithm: 'HS256' });
throw new Error('Expected jwt.sign to error via callback');
} catch (err) {
expect(err).to.be.instanceOf(Error);
expect(String(err.message)).to.match(/secret|private key must have a value/i);
}
});
test('ERROR (callback) — invalid expiresIn format', async function () {
try {
await signViaCallback({ sub: 'bad_exp' }, HS_SECRET, { expiresIn: 'not-a-time' });
throw new Error('Expected jwt.sign to error via callback');
} catch (err) {
expect(err).to.be.instanceOf(Error);
expect(String(err.message)).to.match(/expiresIn/i);
}
});
test('ERROR (callback) — unsupported/invalid algorithm', async function () {
try {
await signViaCallback({ sub: 'bad_alg' }, HS_SECRET, { algorithm: 'FOO256' });
throw new Error('Expected jwt.sign to error via callback');
} catch (err) {
expect(err).to.be.instanceOf(Error);
expect(String(err.message)).to.match(/algorithm/i);
}
});
test('CONTROL (callback) — succeeds when options are valid', async function () {
const token = await jwt.sign({ sub: 'ok' }, HS_SECRET, { algorithm: 'HS256', expiresIn: '10m' });
expect(token).to.be.a('string');
});
}
settings {
encodeUrl: true
}

View File

@@ -1,160 +0,0 @@
meta {
name: sign with callback token
type: http
seq: 3
}
post {
url: {{host}}/api/echo
body: none
auth: inherit
}
tests {
const jwt = require('jsonwebtoken');
const HS_SECRET = 'supersecret';
const payload = { sub: 'user123' };
function once(fn) {
let called = false;
return (...args) => {
if (!called) {
called = true;
fn(...args);
}
};
}
function signAsync(payload, secret, options = {}) {
return new Promise((resolve, reject) => {
jwt.sign(payload, secret, options, (err, token) => {
if (err) reject(err);
else resolve(token);
});
});
}
// ------------------------------------------------------------
// 1. Named Normal Callback
// ------------------------------------------------------------
test('sign — named normal callback', function () {
function signCallback(err, token) {
expect(err).to.be.null;
expect(token).to.be.a('string');
// Verify token to ensure correctness
const decoded = jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] });
expect(decoded.sub).to.equal('user123');
console.log('Named callback signed token:', token);
}
jwt.sign(payload, HS_SECRET, { algorithm: 'HS256', expiresIn: '15m' }, signCallback);
});
// ------------------------------------------------------------
// 2. Anonymous Callback
// ------------------------------------------------------------
test('sign — anonymous callback', function () {
jwt.sign(payload, HS_SECRET, { algorithm: 'HS256' }, function (err, token) {
expect(err).to.be.null;
expect(token).to.be.a('string');
const decoded = jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] });
expect(decoded.sub).to.equal('user123');
console.log('Anonymous callback signed token:', token);
});
});
// ------------------------------------------------------------
// 3. Arrow Function Callback
// ------------------------------------------------------------
test('sign — arrow function callback', function () {
jwt.sign(payload, HS_SECRET, { algorithm: 'HS256' }, (err, token) => {
expect(err).to.be.null;
expect(token).to.be.a('string');
const decoded = jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] });
expect(decoded.sub).to.equal('user123');
console.log('Arrow callback signed token:', token);
});
});
// ------------------------------------------------------------
// 4. Bound Method Callback
// ------------------------------------------------------------
test('sign — bound method callback', function () {
const signer = {
prefix: '[SIGN]',
done(err, token) {
expect(err).to.be.null;
expect(token).to.be.a('string');
const decoded = jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] });
expect(decoded.sub).to.equal('user123');
console.log(this.prefix, 'Bound callback signed token:', token);
},
};
jwt.sign(payload, HS_SECRET, { algorithm: 'HS256' }, signer.done.bind(signer));
});
// ------------------------------------------------------------
// 5. Higher-Order Callback
// ------------------------------------------------------------
function makeSignCallback(label) {
return (err, token) => {
expect(err).to.be.null;
expect(token).to.be.a('string');
const decoded = jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] });
expect(decoded.sub).to.equal('user123');
console.log(label, 'Higher-order callback signed token:', token);
};
}
test('sign — higher-order callback', function () {
const cb = makeSignCallback('[CUSTOM LABEL]');
jwt.sign(payload, HS_SECRET, { algorithm: 'HS256' }, cb);
});
// ------------------------------------------------------------
// 6. Once-Wrapped Callback
// ------------------------------------------------------------
test('sign — once-wrapped callback', function () {
const cb = once((err, token) => {
expect(err).to.be.null;
expect(token).to.be.a('string');
const decoded = jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] });
expect(decoded.sub).to.equal('user123');
console.log('Once callback executed and signed token:', token);
});
jwt.sign(payload, HS_SECRET, { algorithm: 'HS256' }, cb);
});
// ------------------------------------------------------------
// 7. Promise / Async-Await
// ------------------------------------------------------------
test('sign — promise wrapper with async/await', async function () {
const token = await signAsync(payload, HS_SECRET, { algorithm: 'HS256', expiresIn: '15m' });
expect(token).to.be.a('string');
const decoded = jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] });
expect(decoded.sub).to.equal('user123');
console.log('Promise/async signed token:', token);
});
}
settings {
encodeUrl: true
}

View File

@@ -1,109 +0,0 @@
meta {
name: sign
type: http
seq: 1
}
post {
url: {{host}}/api/echo
body: none
auth: inherit
}
script:pre-request {
const jwt = require('jsonwebtoken');
const payload = {
userId: 123,
username: 'testuser',
role: 'admin',
iat: Math.floor(Date.now() / 1000)
};
const secret = bru.getEnvVar('secret');
const options = {
algorithm: 'HS256',
expiresIn: '1h'
};
try {
console.log('Testing JWT encoding...');
const token = jwt.sign(payload, secret, options);
console.log('JWT Token encoded successfully:', token);
bru.setEnvVar('jwt_token', token);
bru.setEnvVar('original_payload', JSON.stringify(payload));
console.log('JWT encoding test passed!');
} catch (error) {
console.error('JWT encoding failed:', error.message);
throw error;
}
}
tests {
const atob = require('atob')
test("JWT token should be generated", function() {
const jwtToken = bru.getEnvVar('jwt_token');
expect(jwtToken).to.exist;
});
test("JWT token should be a string", function() {
const jwtToken = bru.getEnvVar('jwt_token');
expect(typeof jwtToken).to.equal('string');
});
test("JWT token should have 3 parts (header.payload.signature)", function() {
const jwtToken = bru.getEnvVar('jwt_token');
const parts = jwtToken.split('.');
expect(parts.length).to.equal(3);
});
test("JWT token should be valid base64", function() {
const jwtToken = bru.getEnvVar('jwt_token');
const parts = jwtToken.split('.');
// Test that each part is valid base64
parts.forEach((part, index) => {
try {
atob(part);
} catch (e) {
throw new Error(`JWT part ${index + 1} is not valid base64`);
}
});
});
test("JWT token should contain expected payload data", function() {
const jwtToken = bru.getEnvVar('jwt_token');
const originalPayload = JSON.parse(bru.getEnvVar('original_payload'));
// Decode the payload part (second part of JWT)
const parts = jwtToken.split('.');
const payloadPart = parts[1];
const decodedPayload = JSON.parse(atob(payloadPart));
console.log(decodedPayload)
expect(decodedPayload.userId).to.equal(originalPayload.userId);
expect(decodedPayload.username).to.equal(originalPayload.username);
expect(decodedPayload.role).to.equal(originalPayload.role);
});
test("JWT token should have proper header", function() {
const jwtToken = bru.getEnvVar('jwt_token');
const parts = jwtToken.split('.');
const headerPart = parts[0];
const header = JSON.parse(atob(headerPart));
expect(header.alg).to.equal('HS256');
expect(header.typ).to.equal('JWT');
});
}
settings {
encodeUrl: true
}

View File

@@ -1,8 +0,0 @@
meta {
name: verify
seq: 2
}
auth {
mode: inherit
}

View File

@@ -1,85 +0,0 @@
meta {
name: verify with callback err
type: http
seq: 2
}
post {
url: {{host}}/api/echo
body: none
auth: inherit
}
tests {
const jwt = require('jsonwebtoken');
const HS_SECRET = 'supersecret';
function verifyViaCallback(token, secret, options = {}) {
return new Promise((resolve, reject) => {
jwt.verify(token, secret, options, (err, decoded) => {
if (err) return reject(err);
resolve(decoded);
});
});
}
function createValidToken(payload = { sub: 'user123' }, secret = HS_SECRET) {
return jwt.sign(payload, secret, { algorithm: 'HS256', expiresIn: '1h' });
}
/* ============================================================
ERROR TESTS — jwt.verify should call callback with `err`
============================================================ */
test('ERROR (callback) — malformed token', async function () {
const malformedToken = 'abc.def'; // not a valid JWT
try {
await verifyViaCallback(malformedToken, HS_SECRET, { algorithms: ['HS256'] });
throw new Error('Expected jwt.verify to error via callback');
} catch (err) {
expect(err).to.be.instanceOf(Error);
expect(String(err.message)).to.match(/jwt malformed|invalid token/i);
}
});
test('ERROR (callback) — invalid signature (wrong secret)', async function () {
const token = createValidToken(); // signed with HS_SECRET
try {
await verifyViaCallback(token, 'wrong_secret', { algorithms: ['HS256'] });
throw new Error('Expected jwt.verify to error via callback');
} catch (err) {
expect(err).to.be.instanceOf(Error);
expect(String(err.message)).to.match(/invalid signature/i);
}
});
test('ERROR (callback) — invalid algorithm', async function () {
const token = createValidToken();
try {
// Pass unsupported algorithm intentionally
await verifyViaCallback(token, HS_SECRET, { algorithms: ['RS256'] });
throw new Error('Expected jwt.verify to error due to invalid algorithm');
} catch (err) {
expect(err).to.be.instanceOf(Error);
expect(String(err.message)).to.match(/invalid algorithm/i);
}
});
test('ERROR (callback) — missing secret', async function () {
const token = createValidToken();
try {
await verifyViaCallback(token, undefined, { algorithms: ['HS256'] });
throw new Error('Expected jwt.verify to error due to missing secret');
} catch (err) {
expect(err).to.be.instanceOf(Error);
expect(String(err.message)).to.match(/secret|key must be provided/i);
}
});
}
settings {
encodeUrl: true
}

View File

@@ -1,114 +0,0 @@
meta {
name: verify with callback token
type: http
seq: 3
}
post {
url: {{host}}/api/echo
body: none
auth: inherit
}
tests {
const jwt = require('jsonwebtoken');
const HS_SECRET = 'supersecret';
const token = jwt.sign({ sub: 'user123' }, HS_SECRET, {
algorithm: 'HS256',
expiresIn: '15m',
});
function once(fn) {
let called = false;
return (...args) => {
if (!called) {
called = true;
fn(...args);
}
};
}
function verifyAsync(token, secret, options = {}) {
return new Promise((resolve, reject) => {
jwt.verify(token, secret, options, (err, decoded) => {
if (err) reject(err);
else resolve(decoded);
});
});
}
test('verify — named normal callback', function () {
function verifyCallback(err, decoded) {
expect(err).to.be.null;
expect(decoded.sub).to.equal('user123');
console.log('Named callback verified user:', decoded.sub);
}
jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] }, verifyCallback);
});
test('verify — anonymous callback', function () {
jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] }, function (err, decoded) {
expect(err).to.be.null;
expect(decoded.sub).to.equal('user123');
console.log('Anonymous callback verified user:', decoded.sub);
});
});
test('verify — arrow function callback', function () {
jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] }, (err, decoded) => {
expect(err).to.be.null;
expect(decoded.sub).to.equal('user123');
console.log('Arrow callback verified user:', decoded.sub);
});
});
test('verify — bound method callback', function () {
const handler = {
prefix: '[VERIFY]',
done(err, decoded) {
expect(err).to.be.null;
expect(decoded.sub).to.equal('user123');
console.log(this.prefix, 'Bound callback verified user:', decoded.sub);
},
};
jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] }, handler.done.bind(handler));
});
function makeVerifyCallback(label) {
return (err, decoded) => {
expect(err).to.be.null;
expect(decoded.sub).to.equal('user123');
console.log(label, 'Higher-order callback verified user:', decoded.sub);
};
}
test('verify — higher-order callback', function () {
const cb = makeVerifyCallback('[CUSTOM LABEL]');
jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] }, cb);
});
test('verify — once-wrapped callback', function () {
const cb = once((err, decoded) => {
expect(err).to.be.null;
expect(decoded.sub).to.equal('user123');
console.log('Once callback executed and verified user:', decoded.sub);
});
jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] }, cb);
});
test('verify — promise wrapper with async/await', async function () {
const decoded = await verifyAsync(token, HS_SECRET, { algorithms: ['HS256'] });
expect(decoded.sub).to.equal('user123');
console.log('Promise/async verified user:', decoded.sub);
});
}
settings {
encodeUrl: true
}

View File

@@ -1,104 +0,0 @@
meta {
name: verify
type: http
seq: 1
}
post {
url: {{host}}/api/echo
body: none
auth: inherit
}
script:pre-request {
const jwt = require('jsonwebtoken');
const validPayload = {
userId: 789,
username: 'verifyuser',
role: 'admin',
iat: Math.floor(Date.now() / 1000)
};
const secret = bru.getEnvVar('secret') || 'test-secret-key';
const wrongSecret = 'wrong-secret-key';
const validToken = jwt.sign(validPayload, secret, { algorithm: 'HS256', expiresIn: '1h' });
const invalidToken = jwt.sign(validPayload, wrongSecret, { algorithm: 'HS256', expiresIn: '1h' });
bru.setEnvVar('valid_token', validToken);
bru.setEnvVar('invalid_token', invalidToken);
try {
console.log('Testing JWT verification...');
console.log('Valid token:', validToken);
const verified = jwt.verify(validToken, secret);
const verifiedWithOptions = jwt.verify(validToken, secret, {
algorithms: ['HS256'],
ignoreExpiration: false
});
if (!verifiedWithOptions) {
throw new Error('Verification with options should work');
}
console.log('JWT verification test passed!');
bru.setEnvVar('verified_payload', JSON.stringify(verified));
} catch (error) {
console.error('JWT verification failed:', error.message);
throw error;
}
}
tests {
test("Verified payload should exist", function() {
const verifiedPayload = bru.getEnvVar('verified_payload');
expect(verifiedPayload).to.exist;
});
test("Verified payload should be valid JSON", function() {
const verifiedPayload = bru.getEnvVar('verified_payload');
const parsed = JSON.parse(verifiedPayload);
expect(typeof parsed).to.equal('object');
});
test("Verified payload should contain correct user data", function() {
const verifiedPayload = JSON.parse(bru.getEnvVar('verified_payload'));
expect(verifiedPayload.userId).to.equal(789);
expect(verifiedPayload.username).to.equal('verifyuser');
expect(verifiedPayload.role).to.equal('admin');
});
test("Verified payload should have timestamp fields", function() {
const verifiedPayload = JSON.parse(bru.getEnvVar('verified_payload'));
expect(verifiedPayload.iat).to.exist;
expect(verifiedPayload.exp).to.exist;
expect(typeof verifiedPayload.iat).to.equal('number');
expect(typeof verifiedPayload.exp).to.equal('number');
});
test("Invalid token with wrong secret should throw error", function() {
const jwt = require('jsonwebtoken');
const invalidToken = bru.getEnvVar('invalid_token');
const secret = bru.getEnvVar('secret') || 'test-secret-key';
try {
jwt.verify(invalidToken, secret);
expect.fail('Expected JWT verification to throw an error for invalid token');
} catch (error) {
expect(error).to.exist;
expect(error.message).to.equal('invalid signature');
console.log('Invalid token correctly threw error:', error.message);
}
});
}
settings {
encodeUrl: true
}

View File

@@ -1,16 +0,0 @@
{
"maximized": true,
"lastOpenedCollections": ["{{projectRoot}}/tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection"],
"preferences": {
"request": {
"sslVerification": true,
"customCaCertificate": {
"enabled": false,
"filePath": ""
},
"keepDefaultCaCertificates": {
"enabled": true
}
}
}
}

View File

@@ -1,8 +0,0 @@
{
"collections": [
{
"pathname": "{{projectRoot}}/tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection",
"selectedEnvironment": "Prod"
}
]
}

View File

@@ -1,64 +0,0 @@
import { test, expect } from '../../../../playwright';
test.describe.serial('jwt collection success', () => {
test('developer mode', async ({ pageWithUserData: page }) => {
// init dev mode
await page.getByTitle('jsonwebtoken').click();
await page.getByLabel('Developer Mode(use only if').check();
await page.getByRole('button', { name: 'Save' }).click();
test.setTimeout(2 * 60 * 1000);
// Run the collection
await page.locator('.collection-actions').hover();
await page.locator('.collection-actions .icon').click();
await page.getByText('Run', { exact: true }).click();
await page.getByRole('button', { name: 'Run Collection' }).click();
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
// Parse and validate test results
const result = await page.getByText('Total Requests: ').innerText();
const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/);
if (!matches) {
throw new Error('Could not parse test results');
}
const [totalRequests, passed, failed, skipped] = matches.slice(1);
await expect(parseInt(totalRequests)).toBe(7);
await expect(parseInt(passed)).toBe(7);
await expect(parseInt(failed)).toBe(0);
await expect(parseInt(skipped)).toBe(0);
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed));
});
test('safe mode', async ({ pageWithUserData: page }) => {
// init safe mode
await page.getByTitle('jsonwebtoken').click();
await page.getByText('Developer Mode').click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
test.setTimeout(2 * 60 * 1000);
// Run the collection
await page.locator('.collection-actions').hover();
await page.locator('.collection-actions .icon').click();
await page.getByText('Run', { exact: true }).click();
await page.getByRole('button', { name: 'Run Collection' }).click();
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
// Parse and validate test results
const result = await page.getByText('Total Requests: ').innerText();
const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/);
if (!matches) {
throw new Error('Could not parse test results');
}
const [totalRequests, passed, failed, skipped] = matches.slice(1);
await expect(parseInt(totalRequests)).toBe(7);
await expect(parseInt(passed)).toBe(7);
await expect(parseInt(failed)).toBe(0);
await expect(parseInt(skipped)).toBe(0);
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed));
});
});