diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 006a489ed..4dc16f485 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -35,7 +35,7 @@ jobs:
- name: Lint Check
run: npm run lint
env:
- ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event.pull_request.base.ref }}
+ ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || 'main' }}
# tests
- name: Test Package bruno-js
diff --git a/eslint.config.js b/eslint.config.js
index 861eeda1c..4eaaf583b 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -18,6 +18,7 @@ module.exports = runESMImports().then(() => defineConfig([
},
files: [
'./eslint.config.js',
+ 'tests/**/*.spec.{ts,js}',
'packages/bruno-app/**/*.{js,jsx,ts}',
'packages/bruno-app/src/test-utils/mocks/codemirror.js',
'packages/bruno-cli/**/*.js',
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js
index e88406597..1defe9bbf 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js
@@ -193,7 +193,7 @@ const EnvironmentSelector = ({ collection }) => {
{/* Modals - Rendered outside dropdown to avoid conflicts */}
{showGlobalSettings && (
-
+
)}
{showCollectionSettings && }
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
index d91e2eb1c..c16dbc9d9 100644
--- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
@@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash, IconAlertCircle } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
-import { useDispatch } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import MultiLineEditor from 'components/MultiLineEditor/index';
import StyledWrapper from './StyledWrapper';
import { uuid } from 'utils/common';
@@ -12,11 +12,18 @@ import { variableNameRegex } from 'utils/common/regex';
import toast from 'react-hot-toast';
import { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import { Tooltip } from 'react-tooltip';
+import { getGlobalEnvironmentVariables } from 'utils/collections';
-const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentVariables }) => {
+const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentVariables, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const addButtonRef = useRef(null);
+ const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector(state => state.globalEnvironments);
+
+ let _collection = cloneDeep(collection);
+
+ const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
+ _collection.globalEnvironmentVariables = globalEnvironmentVariables;
const formik = useFormik({
enableReinitialize: true,
@@ -93,7 +100,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
useEffect(() => {
if (formik.dirty) {
- // Smooth scrolling to the changed parameter is temporarily disabled
+ // Smooth scrolling to the changed parameter is temporarily disabled
// due to UX issues when editing the first row in a long list of environment variables.
// addButtonRef.current?.scrollIntoView({ behavior: 'smooth' });
}
@@ -149,7 +156,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
{
+const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
const [openEditModal, setOpenEditModal] = useState(false);
const [openDeleteModal, setOpenDeleteModal] = useState(false);
const [openCopyModal, setOpenCopyModal] = useState(false);
@@ -37,7 +37,7 @@ const EnvironmentDetails = ({ environment, setIsModified }) => {
-
+
);
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/index.js
index d04edd838..c99459efe 100644
--- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/index.js
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/EnvironmentList/index.js
@@ -10,7 +10,7 @@ import ImportEnvironment from '../ImportEnvironment';
import { isEqual } from 'lodash';
import ToolHint from 'components/ToolHint/index';
-const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironment, setSelectedEnvironment, isModified, setIsModified }) => {
+const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironment, setSelectedEnvironment, isModified, setIsModified, collection }) => {
const [openCreateModal, setOpenCreateModal] = useState(false);
const [openImportModal, setOpenImportModal] = useState(false);
const [openManageSecretsModal, setOpenManageSecretsModal] = useState(false);
@@ -143,6 +143,7 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
environment={selectedEnvironment}
setIsModified={setIsModified}
originalEnvironmentVariables={originalEnvironmentVariables}
+ collection={collection}
/>
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/index.js
index 3af5570b4..d3e8d16d8 100644
--- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/index.js
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/index.js
@@ -39,7 +39,7 @@ const DefaultTab = ({ setTab }) => {
);
};
-const EnvironmentSettings = ({ globalEnvironments, onClose }) => {
+const EnvironmentSettings = ({ globalEnvironments, collection, onClose }) => {
const [isModified, setIsModified] = useState(false);
const environments = globalEnvironments;
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
@@ -68,6 +68,7 @@ const EnvironmentSettings = ({ globalEnvironments, onClose }) => {
setSelectedEnvironment={setSelectedEnvironment}
isModified={isModified}
setIsModified={setIsModified}
+ collection={collection}
/>
);
diff --git a/packages/bruno-app/src/components/GlobalSearchModal/index.js b/packages/bruno-app/src/components/GlobalSearchModal/index.js
index 5bfb09ff8..a40d1aa8a 100644
--- a/packages/bruno-app/src/components/GlobalSearchModal/index.js
+++ b/packages/bruno-app/src/components/GlobalSearchModal/index.js
@@ -73,7 +73,8 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
const itemPathLower = itemPath.toLowerCase();
if (isItemARequest(item)) {
- const nameMatch = searchTerms.every(term => item.name.toLowerCase().includes(term));
+ // add an optional check for the item name to prevent a crash if it doesn’t exist.
+ const nameMatch = searchTerms.every(term => (item.name || '').toLowerCase().includes(term));
const urlMatch = searchTerms.every(term => (item.request?.url || '').toLowerCase().includes(term));
const pathMatch = enablePathMatch && searchTerms.every(term => itemPathLower.includes(term));
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
index 3152bef8c..fe180d76d 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
@@ -9,6 +9,7 @@ import Dropdown from 'components/Dropdown';
import { toggleCollection } from 'providers/ReduxStore/slices/collections';
import { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch, useSelector } from 'react-redux';
+import { hideHomePage } from 'providers/ReduxStore/slices/app';
import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder';
@@ -92,6 +93,7 @@ const Collection = ({ collection, searchText }) => {
}
if(!isChevronClick) {
+ dispatch(hideHomePage()); // @TODO Playwright tests are often stuck on home page, rather than collection settings tab. Revisit for a proper fix.
dispatch(
addTab({
uid: collection.uid,
@@ -209,7 +211,7 @@ const Collection = ({ collection, searchText }) => {
const folderItems = sortByNameThenSequence(filter(collection.items, (i) => isItemAFolder(i)));
return (
-
+
{showNewRequestModal && setShowNewRequestModal(false)} />}
{showNewFolderModal && setShowNewFolderModal(false)} />}
{showRenameCollectionModal && (
diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js
index a8ac1b812..bff1ac895 100644
--- a/packages/bruno-app/src/utils/url/index.js
+++ b/packages/bruno-app/src/utils/url/index.js
@@ -32,19 +32,24 @@ export const parsePathParams = (url) => {
paths = uri.split('/');
}
- paths = paths.reduce((acc, path) => {
- if (path !== '' && path[0] === ':') {
- let name = path.slice(1, path.length);
- if (name) {
- let isExist = find(acc, (path) => path.name === name);
- if (!isExist) {
- acc.push({ name: path.slice(1, path.length), value: '' });
+ // Enhanced: also match :param inside parentheses and/or quotes
+ const paramRegex = /[:](\w+)/g;
+ const foundParams = new Set();
+ paths.forEach(segment => {
+ let match;
+ while ((match = paramRegex.exec(segment))) {
+ if (match[1]) {
+ // Clean up: remove trailing quotes/parentheses if present
+ let name = match[1].replace(/[')"`]+$/, '');
+ // Remove leading quotes/parentheses if present
+ name = name.replace(/^[('"`]+/, '');
+ if (name && !foundParams.has(name)) {
+ foundParams.add(name);
}
}
}
- return acc;
- }, []);
- return paths;
+ });
+ return Array.from(foundParams).map(name => ({ name, value: '' }));
};
export const splitOnFirst = (str, char) => {
@@ -79,13 +84,25 @@ export const interpolateUrl = ({ url, variables }) => {
export const interpolateUrlPathParams = (url, params) => {
const getInterpolatedBasePath = (pathname, params) => {
+ const regex = /[:](\w+)/g;
return pathname
.split('/')
.map((segment) => {
- if (segment.startsWith(':')) {
- const pathParamName = segment.slice(1);
- const pathParam = params.find((p) => p?.name === pathParamName && p?.type === 'path');
- return pathParam ? pathParam.value : segment;
+
+ if (!segment.startsWith(':')) return segment;
+
+ let match;
+ while ((match = regex.exec(segment))) {
+ if (match[1]) {
+ // Clean up: remove trailing quotes/parentheses if present
+ let name = match[1].replace(/[')"`]+$/, '');
+ // Remove leading quotes/parentheses if present
+ name = name.replace(/^[('"`]+/, '');
+ if (name) {
+ const pathParam = params.find(p => p?.name === name && p?.type === 'path');
+ return pathParam ? pathParam.value : segment;
+ }
+ }
}
return segment;
})
diff --git a/packages/bruno-app/src/utils/url/index.spec.js b/packages/bruno-app/src/utils/url/index.spec.js
index bbcc919c8..ddab88a99 100644
--- a/packages/bruno-app/src/utils/url/index.spec.js
+++ b/packages/bruno-app/src/utils/url/index.spec.js
@@ -43,6 +43,132 @@ describe('Url Utils - parsePathParams', () => {
{ name: 'postId', value: '' }
]);
});
+
+ it('should parse path param inside parentheses and quotes', () => {
+ const params = parsePathParams('https://example.com/ExchangeRates(\':ExchangeRateOID\')');
+ expect(params).toEqual([{ name: 'ExchangeRateOID', value: '' }]);
+ });
+
+ it('should parse path param inside parentheses and no quotes', () => {
+ const params = parsePathParams('https://example.com/ExchangeRates(:ExchangeRateOID)');
+ expect(params).toEqual([{ name: 'ExchangeRateOID', value: '' }]);
+ });
+
+ it('should parse multiple path params inside parentheses', () => {
+ const params = parsePathParams('https://example.com/Exchange(:ExchangeId)/ExchangeRates(:ExchangeRateOID)');
+ expect(params).toEqual([{ name: 'ExchangeId', value: '' }, { name: 'ExchangeRateOID', value: '' }]);
+ });
+
+ it('should parse mix and match of normal and param inside parentheses', () => {
+ const params = parsePathParams('https://example.com/Exchange(:ExchangeId)/:key');
+ expect(params).toEqual([{ name: 'ExchangeId', value: '' }, { name: 'key', value: '' }]);
+ });
+
+ // OData-specific test cases for enhanced path parameter parsing
+ it('should parse OData entity key with single quotes', () => {
+ const params = parsePathParams('https://example.com/odata/Products(\':productId\')');
+ expect(params).toEqual([{ name: 'productId', value: '' }]);
+ });
+
+ it('should parse OData entity key with double quotes', () => {
+ const params = parsePathParams('https://example.com/odata/Products(":productId")');
+ expect(params).toEqual([{ name: 'productId', value: '' }]);
+ });
+
+ it('should parse OData entity key with backticks', () => {
+ const params = parsePathParams('https://example.com/odata/Products(`:productId`)');
+ expect(params).toEqual([{ name: 'productId', value: '' }]);
+ });
+
+ it('should parse OData entity key with parentheses only', () => {
+ const params = parsePathParams('https://example.com/odata/Products(:productId)');
+ expect(params).toEqual([{ name: 'productId', value: '' }]);
+ });
+
+ it('should parse OData composite key with multiple parameters', () => {
+ const params = parsePathParams('https://example.com/odata/Orders(:orderId,ProductId=\':productId\')');
+ expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'productId', value: '' }]);
+ });
+
+ it('should parse OData navigation property with key', () => {
+ const params = parsePathParams('https://example.com/odata/Orders(:orderId)/Items(\':itemId\')');
+ expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }]);
+ });
+
+ it('should parse OData function with parameters', () => {
+ const params = parsePathParams('https://example.com/odata/GetProductsByCategory(categoryId=\':categoryId\')');
+ expect(params).toEqual([{ name: 'categoryId', value: '' }]);
+ });
+
+ it('should parse OData action with complex parameters', () => {
+ const params = parsePathParams('https://example.com/odata/Products(\':productId\')/Rate(rating=:rating,comment=\':comment\')');
+ expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'rating', value: '' }, { name: 'comment', value: '' }]);
+ });
+
+ it('should handle OData parameters with special characters in names', () => {
+ const params = parsePathParams('https://example.com/odata/Products(\':product-id\')');
+ expect(params).toEqual([{ name: 'product', value: '' }]);
+ });
+
+ it('should handle OData parameters with underscores in names', () => {
+ const params = parsePathParams('https://example.com/odata/Products(\':product_id\')');
+ expect(params).toEqual([{ name: 'product_id', value: '' }]);
+ });
+
+ it('should handle OData parameters with mixed quote types', () => {
+ const params = parsePathParams('https://example.com/odata/Products(\':productId\')/Categories(":categoryId")');
+ expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'categoryId', value: '' }]);
+ });
+
+ it('should handle OData parameters with nested parentheses', () => {
+ const params = parsePathParams('https://example.com/odata/Products((\':productId\'))');
+ expect(params).toEqual([{ name: 'productId', value: '' }]);
+ });
+
+ it('should handle OData parameters with complex nested structures', () => {
+ const params = parsePathParams('https://example.com/odata/Orders(:orderId)/Items(\':itemId\')/Properties(\':propName\')');
+ expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }, { name: 'propName', value: '' }]);
+ });
+
+ it('should handle OData parameters with query options in path', () => {
+ const params = parsePathParams('https://example.com/odata/Products(\':productId\')?$expand=Category');
+ expect(params).toEqual([{ name: 'productId', value: '' }]);
+ });
+
+ it('should handle OData parameters with multiple segments and mixed syntax', () => {
+ const params = parsePathParams('https://example.com/odata/Orders(:orderId)/Items(\':itemId\')/Properties(:propName)');
+ expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }, { name: 'propName', value: '' }]);
+ });
+
+ it('should handle OData parameters with empty string values', () => {
+ const params = parsePathParams('https://example.com/odata/Products(\'\')');
+ expect(params).toEqual([]);
+ });
+
+ it('should handle OData parameters with function calls in parentheses', () => {
+ const params = parsePathParams('https://example.com/odata/Products(GetId(\':productId\'))');
+ expect(params).toEqual([{ name: 'productId', value: '' }]);
+ });
+
+ it('should handle OData parameters with escaped quotes', () => {
+ const params = parsePathParams('https://example.com/odata/Products(\'ABC\'\'123\')');
+ expect(params).toEqual([]);
+ });
+
+ it('should handle OData parameters with spaces in quotes', () => {
+ const params = parsePathParams('https://example.com/odata/Products(\'Product Name With Spaces\')');
+ expect(params).toEqual([]);
+ });
+
+ it('should handle OData parameters with numeric keys', () => {
+ const params = parsePathParams('https://example.com/odata/Products(12345)');
+ expect(params).toEqual([]);
+ });
+
+ it('should handle OData parameters with GUID keys', () => {
+ const params = parsePathParams('https://example.com/odata/Products(\'123e4567-e89b-12d3-a456-426614174000\')');
+ expect(params).toEqual([]);
+ });
});
describe('Url Utils - splitOnFirst', () => {
diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js
index 7ec7041b5..83b9e0881 100644
--- a/packages/bruno-cli/src/runner/interpolate-vars.js
+++ b/packages/bruno-cli/src/runner/interpolate-vars.js
@@ -115,16 +115,21 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
throw { message: 'Invalid URL format', originalError: e.message };
}
+ const paramRegex = /[:](\w+)/g;
const interpolatedUrlPath = url.pathname
.split('/')
.filter((path) => path !== '')
.map((path) => {
- if (path[0] !== ':') {
- return '/' + path;
+ const matches = path.match(paramRegex);
+ if (matches) {
+ const paramName = matches[0].slice(1); // Remove the : prefix
+ const existingPathParam = request.pathParams.find(param => param.name === paramName);
+ if (!existingPathParam) {
+ return '/' + path;
+ }
+ return '/' + path.replace(':' + paramName, existingPathParam.value);
} else {
- const name = path.slice(1);
- const existingPathParam = request?.pathParams?.find((param) => param.type === 'path' && param.name === name);
- return existingPathParam ? '/' + existingPathParam.value : '';
+ return '/' + path;
}
})
.join('');
diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js
index 1b5993852..abaed8bf7 100644
--- a/packages/bruno-cli/src/runner/prepare-request.js
+++ b/packages/bruno-cli/src/runner/prepare-request.js
@@ -1,12 +1,18 @@
-const { get, each, filter } = require('lodash');
+const get = require('lodash/get');
+const each = require('lodash/each');
+const filter = require('lodash/filter');
+const find = require('lodash/find');
const decomment = require('decomment');
const crypto = require('node:crypto');
+const fs = require('node:fs/promises');
const { mergeHeaders, mergeScripts, mergeVars, mergeAuth, getTreePathFromCollectionToItem } = require('../utils/collection');
const { buildFormUrlEncodedPayload } = require('../utils/form-data');
+const path = require('node:path');
-const prepareRequest = (item = {}, collection = {}) => {
+const prepareRequest = async (item = {}, collection = {}) => {
const request = item?.request;
const brunoConfig = get(collection, 'brunoConfig', {});
+ const collectionPath = collection?.pathname;
const headers = {};
let contentTypeDefined = false;
@@ -288,6 +294,32 @@ const prepareRequest = (item = {}, collection = {}) => {
axiosRequest.data = request.body.sparql;
}
+ if (request.body.mode === 'file') {
+ if (!contentTypeDefined) {
+ axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads
+ }
+
+ const bodyFile = find(request.body.file, param => param.selected);
+ if (bodyFile) {
+ let { filePath, contentType } = bodyFile;
+
+ axiosRequest.headers['content-type'] = contentType;
+
+ if (filePath) {
+ if (!path.isAbsolute(filePath)) {
+ filePath = path.join(collectionPath, filePath);
+ }
+
+ try {
+ const fileContent = await fs.readFile(filePath);
+ axiosRequest.data = fileContent;
+ } catch (error) {
+ console.error('Error reading file:', error);
+ }
+ }
+ }
+ }
+
if (request.body.mode === 'formUrlEncoded') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js
index fc7a7f6d6..40f34a910 100644
--- a/packages/bruno-cli/src/runner/run-single-request.js
+++ b/packages/bruno-cli/src/runner/run-single-request.js
@@ -72,7 +72,7 @@ const runSingleRequest = async function (
let preRequestTestResults = [];
let postResponseTestResults = [];
- request = prepareRequest(item, collection);
+ request = await prepareRequest(item, collection);
request.__bruno__executionMode = 'cli';
diff --git a/packages/bruno-cli/tests/runner/prepare-request.spec.js b/packages/bruno-cli/tests/runner/prepare-request.spec.js
index d532dcff1..8056f17bb 100644
--- a/packages/bruno-cli/tests/runner/prepare-request.spec.js
+++ b/packages/bruno-cli/tests/runner/prepare-request.spec.js
@@ -8,7 +8,7 @@ describe('prepare-request: prepareRequest', () => {
const expected = `{
\"test\": \"{{someVar}}\"
}`;
- const result = prepareRequest({ request: { body } });
+ const result = await prepareRequest({ request: { body } });
expect(result.data).toEqual(expected);
});
@@ -17,7 +17,7 @@ describe('prepare-request: prepareRequest', () => {
const expected = `{
\"test\": {{someVar}}
}`;
- const result = prepareRequest({ request: { body } });
+ const result = await prepareRequest({ request: { body } });
expect(result.data).toEqual(expected);
});
});
@@ -56,7 +56,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('API Key Authentication', () => {
- it('If collection auth is apikey in header', () => {
+ it('If collection auth is apikey in header', async () => {
collection.root.request.auth = {
mode: "apikey",
apikey: {
@@ -66,11 +66,11 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item, collection);
+ const result = await prepareRequest(item, collection);
expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');
});
- it('If collection auth is apikey in header and request has existing headers', () => {
+ it('If collection auth is apikey in header and request has existing headers', async () => {
collection.root.request.auth = {
mode: "apikey",
apikey: {
@@ -81,12 +81,12 @@ describe('prepare-request: prepareRequest', () => {
};
item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });
- const result = prepareRequest(item, collection);
+ const result = await prepareRequest(item, collection);
expect(result.headers).toHaveProperty('Content-Type', 'application/json');
expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');
});
- it('If collection auth is apikey in query parameters', () => {
+ it('If collection auth is apikey in query parameters', async () => {
collection.root.request.auth = {
mode: "apikey",
apikey: {
@@ -100,13 +100,13 @@ describe('prepare-request: prepareRequest', () => {
urlObj.searchParams.set(collection.root.request.auth.apikey.key, collection.root.request.auth.apikey.value);
const expected = urlObj.toString();
- const result = prepareRequest(item, collection);
+ const result = await prepareRequest(item, collection);
expect(result.url).toEqual(expected);
});
});
describe('Basic Authentication', () => {
- it('If collection auth is basic auth', () => {
+ it('If collection auth is basic auth', async () => {
collection.root.request.auth = {
mode: 'basic',
basic: {
@@ -115,14 +115,14 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item, collection);
+ const result = await prepareRequest(item, collection);
const expected = { username: 'testUser', password: 'testPass123' };
expect(result.basicAuth).toEqual(expected);
});
});
describe('Bearer Token Authentication', () => {
- it('If collection auth is bearer token', () => {
+ it('If collection auth is bearer token', async () => {
collection.root.request.auth = {
mode: 'bearer',
bearer: {
@@ -130,11 +130,11 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item, collection);
+ const result = await prepareRequest(item, collection);
expect(result.headers).toHaveProperty('Authorization', 'Bearer token');
});
- it('If collection auth is bearer token and request has existing headers', () => {
+ it('If collection auth is bearer token and request has existing headers', async () => {
collection.root.request.auth = {
mode: 'bearer',
bearer: {
@@ -144,14 +144,14 @@ describe('prepare-request: prepareRequest', () => {
item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });
- const result = prepareRequest(item, collection);
+ const result = await prepareRequest(item, collection);
expect(result.headers).toHaveProperty('Authorization', 'Bearer token');
expect(result.headers).toHaveProperty('Content-Type', 'application/json');
});
});
describe('OAuth2 Authentication', () => {
- it('If collection auth is OAuth2 with client credentials grant type', () => {
+ it('If collection auth is OAuth2 with client credentials grant type', async () => {
collection.root.request.auth = {
mode: 'oauth2',
oauth2: {
@@ -167,7 +167,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item, collection);
+ const result = await prepareRequest(item, collection);
expect(result.oauth2).toBeDefined();
expect(result.oauth2.grantType).toBe('client_credentials');
@@ -181,7 +181,7 @@ describe('prepare-request: prepareRequest', () => {
expect(result.oauth2.tokenQueryKey).toBe('access_token');
});
- it('If collection auth is OAuth2 with password grant type', () => {
+ it('If collection auth is OAuth2 with password grant type', async () => {
collection.root.request.auth = {
mode: 'oauth2',
oauth2: {
@@ -199,7 +199,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item, collection);
+ const result = await prepareRequest(item, collection);
expect(result.oauth2).toBeDefined();
expect(result.oauth2.grantType).toBe('password');
@@ -217,7 +217,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('AWS v4 Authentication', () => {
- it('If collection auth is AWS v4', () => {
+ it('If collection auth is AWS v4', async () => {
collection.root.request.auth = {
mode: 'awsv4',
awsv4: {
@@ -230,7 +230,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item, collection);
+ const result = await prepareRequest(item, collection);
const expected = {
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
@@ -244,7 +244,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('NTLM Authentication', () => {
- it('If collection auth is NTLM', () => {
+ it('If collection auth is NTLM', async () => {
collection.root.request.auth = {
mode: 'ntlm',
ntlm: {
@@ -254,7 +254,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item, collection);
+ const result = await prepareRequest(item, collection);
const expected = {
username: 'testUser',
password: 'testPass123',
@@ -265,7 +265,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('WSSE Authentication', () => {
- it('If collection auth is WSSE', () => {
+ it('If collection auth is WSSE', async () => {
collection.root.request.auth = {
mode: 'wsse',
wsse: {
@@ -274,7 +274,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item, collection);
+ const result = await prepareRequest(item, collection);
expect(result.headers).toHaveProperty('X-WSSE');
expect(result.headers['X-WSSE']).toContain('UsernameToken Username="testUser"');
expect(result.headers['X-WSSE']).toContain('PasswordDigest="');
@@ -284,7 +284,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('Digest Authentication', () => {
- it('If collection auth is digest auth', () => {
+ it('If collection auth is digest auth', async () => {
collection.root.request.auth = {
mode: 'digest',
digest: {
@@ -293,7 +293,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item, collection);
+ const result = await prepareRequest(item, collection);
const expected = {
username: 'testUser',
@@ -304,7 +304,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('No Authentication', () => {
- it('If request does not have auth configured', () => {
+ it('If request does not have auth configured', async () => {
delete item.request.auth;
let result;
expect(() => {
@@ -339,7 +339,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('API Key Authentication', () => {
- it('If request auth is apikey in header', () => {
+ it('If request auth is apikey in header', async () => {
item.request.auth = {
mode: "apikey",
apikey: {
@@ -349,11 +349,11 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item);
+ const result = await prepareRequest(item);
expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');
});
- it('If request auth is apikey in header and request has existing headers', () => {
+ it('If request auth is apikey in header and request has existing headers', async () => {
item.request.auth = {
mode: "apikey",
apikey: {
@@ -364,12 +364,12 @@ describe('prepare-request: prepareRequest', () => {
};
item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });
- const result = prepareRequest(item);
+ const result = await prepareRequest(item);
expect(result.headers).toHaveProperty('Content-Type', 'application/json');
expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');
});
- it('If request auth is apikey in query parameters', () => {
+ it('If request auth is apikey in query parameters', async () => {
item.request.auth = {
mode: "apikey",
apikey: {
@@ -383,13 +383,13 @@ describe('prepare-request: prepareRequest', () => {
urlObj.searchParams.set(item.request.auth.apikey.key, item.request.auth.apikey.value);
const expected = urlObj.toString();
- const result = prepareRequest(item);
+ const result = await prepareRequest(item);
expect(result.url).toEqual(expected);
});
});
describe('Basic Authentication', () => {
- it('If request auth is basic auth', () => {
+ it('If request auth is basic auth', async () => {
item.request.auth = {
mode: 'basic',
basic: {
@@ -398,14 +398,14 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item);
+ const result = await prepareRequest(item);
const expected = { username: 'testUser', password: 'testPass123' };
expect(result.basicAuth).toEqual(expected);
});
});
describe('Bearer Token Authentication', () => {
- it('If request auth is bearer token', () => {
+ it('If request auth is bearer token', async () => {
item.request.auth = {
mode: 'bearer',
bearer: {
@@ -413,11 +413,11 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item);
+ const result = await prepareRequest(item);
expect(result.headers).toHaveProperty('Authorization', 'Bearer token123');
});
- it('If request auth is bearer token and request has existing headers', () => {
+ it('If request auth is bearer token and request has existing headers', async () => {
item.request.auth = {
mode: 'bearer',
bearer: {
@@ -427,14 +427,14 @@ describe('prepare-request: prepareRequest', () => {
item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });
- const result = prepareRequest(item);
+ const result = await prepareRequest(item);
expect(result.headers).toHaveProperty('Authorization', 'Bearer token123');
expect(result.headers).toHaveProperty('Content-Type', 'application/json');
});
});
describe('AWS v4 Authentication', () => {
- it('If request auth is AWS v4', () => {
+ it('If request auth is AWS v4', async () => {
item.request.auth = {
mode: 'awsv4',
awsv4: {
@@ -447,7 +447,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item);
+ const result = await prepareRequest(item);
const expected = {
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
@@ -461,7 +461,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('NTLM Authentication', () => {
- it('If request auth is NTLM', () => {
+ it('If request auth is NTLM', async () => {
item.request.auth = {
mode: 'ntlm',
ntlm: {
@@ -471,7 +471,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item);
+ const result = await prepareRequest(item);
const expected = {
username: 'testUser',
password: 'testPass123',
@@ -482,7 +482,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('WSSE Authentication', () => {
- it('If request auth is WSSE', () => {
+ it('If request auth is WSSE', async () => {
item.request.auth = {
mode: 'wsse',
wsse: {
@@ -491,7 +491,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item);
+ const result = await prepareRequest(item);
expect(result.headers).toHaveProperty('X-WSSE');
expect(result.headers['X-WSSE']).toContain('UsernameToken Username="requestUser"');
expect(result.headers['X-WSSE']).toContain('PasswordDigest="');
@@ -501,7 +501,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('Digest Authentication', () => {
- it('If request auth is digest auth', () => {
+ it('If request auth is digest auth', async () => {
item.request.auth = {
mode: 'digest',
digest: {
@@ -510,7 +510,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
- const result = prepareRequest(item);
+ const result = await prepareRequest(item);
const expected = {
username: 'requestUser',
password: 'requestPass123'
@@ -519,4 +519,39 @@ describe('prepare-request: prepareRequest', () => {
});
});
});
+
+ describe('Request file body mode', () => {
+ it('reads the uploaded file and applies correct headers', async () => {
+ const fsPromises = require('node:fs/promises');
+ // Mock fs.readFile to avoid actual file system dependency
+ jest.spyOn(fsPromises, 'readFile').mockResolvedValue(Buffer.from('dummy file content'));
+
+ const body = {
+ mode: 'file',
+ file: [
+ {
+ contentType: 'text/plain',
+ filePath: '/absolute/path/to/file.txt',
+ selected: true,
+ },
+ ],
+ };
+
+ const item = {
+ name: 'File Request',
+ type: 'http-request',
+ request: {
+ method: 'POST',
+ headers: [],
+ params: [],
+ url: 'https://example.com/upload',
+ body,
+ },
+ };
+
+ const result = await prepareRequest(item);
+ expect(result.data).toBeInstanceOf(Buffer);
+ expect(result.headers['content-type']).toBe('text/plain');
+ });
+ });
});
diff --git a/packages/bruno-converters/src/openapi/openapi-to-bruno.js b/packages/bruno-converters/src/openapi/openapi-to-bruno.js
index 79438c231..27811dc6c 100644
--- a/packages/bruno-converters/src/openapi/openapi-to-bruno.js
+++ b/packages/bruno-converters/src/openapi/openapi-to-bruno.js
@@ -38,7 +38,7 @@ const buildEmptyJsonBody = (bodySchema, visited = new Map()) => {
return _jsonBody;
};
-const transformOpenapiRequestItem = (request) => {
+const transformOpenapiRequestItem = (request, usedNames = new Set()) => {
let _operationObject = request.operationObject;
let operationName = _operationObject.summary || _operationObject.operationId || _operationObject.description;
@@ -46,6 +46,27 @@ const transformOpenapiRequestItem = (request) => {
operationName = `${request.method} ${request.path}`;
}
+ // Sanitize operation name to prevent Bruno parsing issues
+ if (operationName) {
+ // Replace line breaks and normalize whitespace
+ operationName = operationName.replace(/[\r\n\s]+/g, ' ').trim();
+ }
+ if (usedNames.has(operationName)) {
+ // Make name unique to prevent filename collisions
+ // Try adding method info first
+ let uniqueName = `${operationName} (${request.method.toUpperCase()})`;
+
+ // If still not unique, add counter
+ let counter = 1;
+ while (usedNames.has(uniqueName)) {
+ uniqueName = `${operationName} (${counter})`;
+ counter++;
+ }
+
+ operationName = uniqueName;
+ }
+ usedNames.add(operationName);
+
// replace OpenAPI links in path by Bruno variables
let path = request.path.replace(/{([a-zA-Z]+)}/g, `{{${_operationObject.operationId}_$1}}`);
@@ -419,6 +440,8 @@ const openAPIRuntimeExpressionToScript = (expression) => {
};
export const parseOpenApiCollection = (data) => {
+ const usedNames = new Set();
+
const brunoCollection = {
name: '',
uid: uuid(),
@@ -512,11 +535,11 @@ export const parseOpenApiCollection = (data) => {
name: group.name
}
},
- items: group.requests.map(transformOpenapiRequestItem)
+ items: group.requests.map(req => transformOpenapiRequestItem(req, usedNames)),
};
});
- let ungroupedItems = ungroupedRequests.map(transformOpenapiRequestItem);
+ let ungroupedItems = ungroupedRequests.map(req => transformOpenapiRequestItem(req, usedNames));
let brunoCollectionItems = brunoFolders.concat(ungroupedItems);
brunoCollection.items = brunoCollectionItems;
diff --git a/packages/bruno-electron/src/app/onboarding.js b/packages/bruno-electron/src/app/onboarding.js
index 5c9e4a492..de7b575e2 100644
--- a/packages/bruno-electron/src/app/onboarding.js
+++ b/packages/bruno-electron/src/app/onboarding.js
@@ -77,11 +77,12 @@ async function onboardUser(mainWindow, lastOpenedCollections) {
}
if (process.env.DISABLE_SAMPLE_COLLECTION_IMPORT !== 'true') {
- // Onboarding was added later;
- // if a collection already exists, user is old → skip onboarding
+ // Check if user already has collections (indicates they're an existing user)
+ // Onboarding was added in a later version, so for existing users we should skip it
+ // to avoid creating sample collections
const collections = await lastOpenedCollections.getAll();
if (collections.length > 0) {
- preferencesUtil.markAsLaunched();
+ await preferencesUtil.markAsLaunched();
return;
}
@@ -89,11 +90,11 @@ async function onboardUser(mainWindow, lastOpenedCollections) {
await importSampleCollection(collectionLocation, mainWindow, lastOpenedCollections);
}
- preferencesUtil.markAsLaunched();
+ await preferencesUtil.markAsLaunched();
} catch (error) {
console.error('Failed to handle onboarding:', error);
// Still mark as launched to prevent retry on next startup
- preferencesUtil.markAsLaunched();
+ await preferencesUtil.markAsLaunched();
}
}
diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
index 0aff18f19..a3b869156 100644
--- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js
+++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
@@ -91,15 +91,15 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
if (typeof request.data === 'string') {
if (request.data.length) {
request.data = _interpolate(request.data, {
- escapeJSONStrings: true
- });
+ escapeJSONStrings: true,
+ });
}
} else if (typeof request.data === 'object') {
try {
const jsonDoc = JSON.stringify(request.data);
const parsed = _interpolate(jsonDoc, {
- escapeJSONStrings: true
- });
+ escapeJSONStrings: true,
+ });
request.data = JSON.parse(parsed);
} catch (err) {}
}
@@ -142,16 +142,21 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
throw { message: 'Invalid URL format', originalError: e.message };
}
+ const paramRegex = /[:](\w+)/g;
const urlPathnameInterpolatedWithPathParams = url.pathname
.split('/')
.filter((path) => path !== '')
.map((path) => {
- if (path[0] !== ':') {
- return '/' + path;
+ const matches = path.match(paramRegex);
+ if (matches) {
+ const paramName = matches[0].slice(1); // Remove the : prefix
+ const existingPathParam = request.pathParams.find(param => param.name === paramName);
+ if (!existingPathParam) {
+ return '/' + path;
+ }
+ return '/' + path.replace(':' + paramName, existingPathParam.value);
} else {
- const name = path.slice(1);
- const existingPathParam = request.pathParams.find((param) => param.type === 'path' && param.name === name);
- return existingPathParam ? '/' + existingPathParam.value : '';
+ return '/' + path;
}
})
.join('');
diff --git a/packages/bruno-electron/src/store/preferences.js b/packages/bruno-electron/src/store/preferences.js
index 44600907a..004d0e860 100644
--- a/packages/bruno-electron/src/store/preferences.js
+++ b/packages/bruno-electron/src/store/preferences.js
@@ -186,10 +186,15 @@ const preferencesUtil = {
hasLaunchedBefore: () => {
return get(getPreferences(), 'onboarding.hasLaunchedBefore', false);
},
- markAsLaunched: () => {
+ markAsLaunched: async () => {
const preferences = getPreferences();
preferences.onboarding.hasLaunchedBefore = true;
- preferencesStore.savePreferences(preferences);
+
+ try {
+ await savePreferences(preferences);
+ } catch (err) {
+ console.error('Failed to save preferences in markAsLaunched:', err);
+ }
}
};
diff --git a/packages/bruno-electron/tests/network/interpolate-vars.spec.js b/packages/bruno-electron/tests/network/interpolate-vars.spec.js
index 88abdd811..0c1e4f178 100644
--- a/packages/bruno-electron/tests/network/interpolate-vars.spec.js
+++ b/packages/bruno-electron/tests/network/interpolate-vars.spec.js
@@ -154,6 +154,33 @@ describe('interpolate-vars: interpolateVars', () => {
const result = interpolateVars(request, null, null, null);
expect(result.url).toBe('http://example.com/foobar');
});
+
+ it('updates the path with odata style params | smoke', async () => {
+ const request = {
+ method: 'GET',
+ url: 'http://example.com/Category(\':CategoryID\')/Item(:ItemId)/:xpath/Tags("tag test")',
+ pathParams: [
+ {
+ type: 'path',
+ name: 'CategoryID',
+ value: 'foobar',
+ },
+ {
+ type: 'path',
+ name: 'ItemId',
+ value: 1,
+ },
+ {
+ type: 'path',
+ name: 'xpath',
+ value: 'foobar',
+ },
+ ],
+ };
+
+ const result = interpolateVars(request, null, null, null);
+ expect(result.url).toBe('http://example.com/Category(\'foobar\')/Item(1)/foobar/Tags(%22tag%20test%22)');
+ });
});
describe('With process environment variables', () => {
diff --git a/packages/bruno-tests/src/echo/index.js b/packages/bruno-tests/src/echo/index.js
index 00b50bd36..a9425b305 100644
--- a/packages/bruno-tests/src/echo/index.js
+++ b/packages/bruno-tests/src/echo/index.js
@@ -1,6 +1,10 @@
const express = require('express');
const router = express.Router();
+router.get('/path/*', (req, res) => {
+ return res.json({ url: req.url });
+});
+
router.post('/json', (req, res) => {
return res.json(req.body);
});
diff --git a/tests/environments/import-environment/global-env-import.spec.ts b/tests/environments/import-environment/global-env-import.spec.ts
index 73e5d7af0..5b545c896 100644
--- a/tests/environments/import-environment/global-env-import.spec.ts
+++ b/tests/environments/import-environment/global-env-import.spec.ts
@@ -63,7 +63,7 @@ test.describe('Global Environment Import Tests', () => {
await page.getByText('×').click();
// Test GET request with global environment
- await page.locator('.collection-item-name').first().click();
+ await page.locator('#collection-environment-test-collection .collection-item-name').first().click();
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts/{{userId}}');
await page.locator('[data-testid="send-arrow-icon"]').click();
await page.locator('[data-testid="response-status-code"]').waitFor({ state: 'visible' });
@@ -74,7 +74,7 @@ test.describe('Global Environment Import Tests', () => {
await expect(responsePane).toContainText('"userId": 1');
// Test POST request
- await page.locator('.collection-item-name').nth(1).click();
+ await page.locator('#collection-environment-test-collection .collection-item-name').nth(1).click();
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts');
await page.locator('[data-testid="send-arrow-icon"]').click();
await page.locator('[data-testid="response-status-code"]').waitFor({ state: 'visible' });
@@ -89,7 +89,5 @@ test.describe('Global Environment Import Tests', () => {
.click();
await page.locator('.dropdown-item').filter({ hasText: 'Close' }).click();
await page.getByRole('button', { name: 'Close' }).click();
-
- await page.locator('.bruno-logo').click();
});
});
diff --git a/tests/environments/multiline-variables/write-multiline-variable.spec.ts b/tests/environments/multiline-variables/write-multiline-variable.spec.ts
index cb4cdb7ae..1127ed369 100644
--- a/tests/environments/multiline-variables/write-multiline-variable.spec.ts
+++ b/tests/environments/multiline-variables/write-multiline-variable.spec.ts
@@ -88,4 +88,8 @@ test.describe('Multiline Variables - Write Test', () => {
fs.writeFileSync(testBruPath, content);
});
+
+ test.afterAll(async ({ page }) => {
+ await page.locator('.bruno-logo').click();
+ });
});
diff --git a/tests/import/test-data/bruno-invalid-corrupted.json b/tests/import/bruno/fixtures/bruno-invalid-corrupted.json
similarity index 100%
rename from tests/import/test-data/bruno-invalid-corrupted.json
rename to tests/import/bruno/fixtures/bruno-invalid-corrupted.json
diff --git a/tests/import/test-data/bruno-malformed.json b/tests/import/bruno/fixtures/bruno-malformed.json
similarity index 100%
rename from tests/import/test-data/bruno-malformed.json
rename to tests/import/bruno/fixtures/bruno-malformed.json
diff --git a/tests/import/test-data/bruno-missing-required-fields.json b/tests/import/bruno/fixtures/bruno-missing-required-fields.json
similarity index 100%
rename from tests/import/test-data/bruno-missing-required-fields.json
rename to tests/import/bruno/fixtures/bruno-missing-required-fields.json
diff --git a/tests/import/test-data/bruno-testbench.json b/tests/import/bruno/fixtures/bruno-testbench.json
similarity index 100%
rename from tests/import/test-data/bruno-testbench.json
rename to tests/import/bruno/fixtures/bruno-testbench.json
diff --git a/tests/import/bruno/002-import-bruno-corrupted-fails.spec.ts b/tests/import/bruno/import-bruno-corrupted-fails.spec.ts
similarity index 89%
rename from tests/import/bruno/002-import-bruno-corrupted-fails.spec.ts
rename to tests/import/bruno/import-bruno-corrupted-fails.spec.ts
index 9d6d10bf4..6fe2a0799 100644
--- a/tests/import/bruno/002-import-bruno-corrupted-fails.spec.ts
+++ b/tests/import/bruno/import-bruno-corrupted-fails.spec.ts
@@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Import Corrupted Bruno Collection - Should Fail', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Import Bruno collection with invalid JSON structure should fail', async ({ page }) => {
- const brunoFile = path.join(testDataDir, 'bruno-malformed.json');
+ const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-malformed.json');
await page.getByRole('button', { name: 'Import Collection' }).click();
diff --git a/tests/import/bruno/003-import-bruno-missing-required-schema.spec.ts b/tests/import/bruno/import-bruno-missing-required-schema.spec.ts
similarity index 88%
rename from tests/import/bruno/003-import-bruno-missing-required-schema.spec.ts
rename to tests/import/bruno/import-bruno-missing-required-schema.spec.ts
index 0f76559f6..e3c73269b 100644
--- a/tests/import/bruno/003-import-bruno-missing-required-schema.spec.ts
+++ b/tests/import/bruno/import-bruno-missing-required-schema.spec.ts
@@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Import Bruno Collection - Missing Required Schema Fields', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Import Bruno collection missing required version field should fail', async ({ page }) => {
- const brunoFile = path.join(testDataDir, 'bruno-missing-required-fields.json');
+ const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-missing-required-fields.json');
await page.getByRole('button', { name: 'Import Collection' }).click();
diff --git a/tests/import/bruno/001-import-bruno-testbench.spec.ts b/tests/import/bruno/import-bruno-testbench.spec.ts
similarity index 91%
rename from tests/import/bruno/001-import-bruno-testbench.spec.ts
rename to tests/import/bruno/import-bruno-testbench.spec.ts
index cb436c1dd..848c8f980 100644
--- a/tests/import/bruno/001-import-bruno-testbench.spec.ts
+++ b/tests/import/bruno/import-bruno-testbench.spec.ts
@@ -1,17 +1,14 @@
import { test, expect } from '../../../playwright';
import * as path from 'path';
-
test.describe('Import Bruno Testbench Collection', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test.beforeAll(async ({ page }) => {
// Navigate back to homescreen after all tests
await page.locator('.bruno-logo').click();
});
test('Import Bruno Testbench collection successfully', async ({ page }) => {
- const brunoFile = path.join(testDataDir, 'bruno-testbench.json');
+ const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-testbench.json');
await page.getByRole('button', { name: 'Import Collection' }).click();
diff --git a/tests/import/file-types/001-file-input-acceptance.spec.ts b/tests/import/file-types/file-input-acceptance.spec.ts
similarity index 100%
rename from tests/import/file-types/001-file-input-acceptance.spec.ts
rename to tests/import/file-types/file-input-acceptance.spec.ts
diff --git a/tests/import/test-data/invalid.txt b/tests/import/file-types/fixtures/invalid.txt
similarity index 100%
rename from tests/import/test-data/invalid.txt
rename to tests/import/file-types/fixtures/invalid.txt
diff --git a/tests/import/file-types/002-invalid-file-handling.spec.ts b/tests/import/file-types/invalid-file-handling.spec.ts
similarity index 77%
rename from tests/import/file-types/002-invalid-file-handling.spec.ts
rename to tests/import/file-types/invalid-file-handling.spec.ts
index 4284bf921..e8439fa51 100644
--- a/tests/import/file-types/002-invalid-file-handling.spec.ts
+++ b/tests/import/file-types/invalid-file-handling.spec.ts
@@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Invalid File Handling', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Handle invalid file without crashing', async ({ page }) => {
- const invalidFile = path.join(testDataDir, 'invalid.txt');
+ const invalidFile = path.resolve(__dirname, 'fixtures', 'invalid.txt');
await page.getByRole('button', { name: 'Import Collection' }).click();
@@ -19,7 +17,7 @@ test.describe('Invalid File Handling', () => {
// Wait for the loader to disappear
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
- const hasError = await page.getByText("Failed to parse the file – ensure it is valid JSON or YAML").first().isVisible();
+ const hasError = await page.getByText('Failed to parse the file – ensure it is valid JSON or YAML').first().isVisible();
expect(hasError).toBe(true);
// Cleanup: close any open modals
diff --git a/tests/import/test-data/insomnia-malformed.json b/tests/import/insomnia/fixtures/insomnia-malformed.json
similarity index 100%
rename from tests/import/test-data/insomnia-malformed.json
rename to tests/import/insomnia/fixtures/insomnia-malformed.json
diff --git a/tests/import/test-data/insomnia-v4.json b/tests/import/insomnia/fixtures/insomnia-v4.json
similarity index 100%
rename from tests/import/test-data/insomnia-v4.json
rename to tests/import/insomnia/fixtures/insomnia-v4.json
diff --git a/tests/import/test-data/insomnia-v5-invalid-missing-collection.yaml b/tests/import/insomnia/fixtures/insomnia-v5-invalid-missing-collection.yaml
similarity index 100%
rename from tests/import/test-data/insomnia-v5-invalid-missing-collection.yaml
rename to tests/import/insomnia/fixtures/insomnia-v5-invalid-missing-collection.yaml
diff --git a/tests/import/test-data/insomnia-v5.yaml b/tests/import/insomnia/fixtures/insomnia-v5.yaml
similarity index 100%
rename from tests/import/test-data/insomnia-v5.yaml
rename to tests/import/insomnia/fixtures/insomnia-v5.yaml
diff --git a/tests/import/insomnia/001-import-insomnia-v4.spec.ts b/tests/import/insomnia/import-insomnia-v4.spec.ts
similarity index 90%
rename from tests/import/insomnia/001-import-insomnia-v4.spec.ts
rename to tests/import/insomnia/import-insomnia-v4.spec.ts
index 1afeed306..8975fe479 100644
--- a/tests/import/insomnia/001-import-insomnia-v4.spec.ts
+++ b/tests/import/insomnia/import-insomnia-v4.spec.ts
@@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Import Insomnia Collection v4', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Import Insomnia Collection v4 successfully', async ({ page }) => {
- const insomniaFile = path.join(testDataDir, 'insomnia-v4.json');
+ const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v4.json');
await page.getByRole('button', { name: 'Import Collection' }).click();
diff --git a/tests/import/insomnia/002-import-insomnia-v5.spec.ts b/tests/import/insomnia/import-insomnia-v5.spec.ts
similarity index 90%
rename from tests/import/insomnia/002-import-insomnia-v5.spec.ts
rename to tests/import/insomnia/import-insomnia-v5.spec.ts
index b246a2454..e875756a3 100644
--- a/tests/import/insomnia/002-import-insomnia-v5.spec.ts
+++ b/tests/import/insomnia/import-insomnia-v5.spec.ts
@@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Import Insomnia Collection v5', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Import Insomnia Collection v5 successfully', async ({ page }) => {
- const insomniaFile = path.join(testDataDir, 'insomnia-v5.yaml');
+ const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v5.yaml');
await page.getByRole('button', { name: 'Import Collection' }).click();
diff --git a/tests/import/insomnia/003-invalid-missing-collection.spec.ts b/tests/import/insomnia/invalid-missing-collection.spec.ts
similarity index 87%
rename from tests/import/insomnia/003-invalid-missing-collection.spec.ts
rename to tests/import/insomnia/invalid-missing-collection.spec.ts
index 0237a8240..f0085f9a8 100644
--- a/tests/import/insomnia/003-invalid-missing-collection.spec.ts
+++ b/tests/import/insomnia/invalid-missing-collection.spec.ts
@@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Invalid Insomnia Collection - Missing Collection Array', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Handle Insomnia v5 collection missing collection array', async ({ page }) => {
- const insomniaFile = path.join(testDataDir, 'insomnia-v5-invalid-missing-collection.yaml');
+ const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v5-invalid-missing-collection.yaml');
await page.getByRole('button', { name: 'Import Collection' }).click();
diff --git a/tests/import/insomnia/004-malformed-structure.spec.ts b/tests/import/insomnia/malformed-structure.spec.ts
similarity index 87%
rename from tests/import/insomnia/004-malformed-structure.spec.ts
rename to tests/import/insomnia/malformed-structure.spec.ts
index 644bd35fe..05e9be062 100644
--- a/tests/import/insomnia/004-malformed-structure.spec.ts
+++ b/tests/import/insomnia/malformed-structure.spec.ts
@@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Invalid Insomnia Collection - Malformed Structure', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Handle malformed Insomnia collection structure', async ({ page }) => {
- const insomniaFile = path.join(testDataDir, 'insomnia-malformed.json');
+ const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-malformed.json');
await page.getByRole('button', { name: 'Import Collection' }).click();
@@ -20,7 +18,7 @@ test.describe('Invalid Insomnia Collection - Malformed Structure', () => {
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
// Check for error message - this should fail during JSON parsing
- const hasError = await page.getByText('Failed to parse the file').isVisible();
+ const hasError = await page.getByText('Failed to parse the file').first().isVisible();
expect(hasError).toBe(true);
// Cleanup: close any open modals
diff --git a/tests/import/openapi/duplicate-operation-names-fix.spec.ts b/tests/import/openapi/duplicate-operation-names-fix.spec.ts
new file mode 100644
index 000000000..cc6e38742
--- /dev/null
+++ b/tests/import/openapi/duplicate-operation-names-fix.spec.ts
@@ -0,0 +1,50 @@
+import { test, expect } from '../../../playwright';
+import * as path from 'path';
+
+test.describe('OpenAPI Duplicate Names Handling', () => {
+ test('should handle duplicate operation names', async ({ page, createTmpDir }) => {
+ const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-duplicate-operation-name.yaml');
+
+ // start the import process
+ await page.getByRole('button', { name: 'Import Collection' }).click();
+
+ // wait for the import collection modal to appear
+ const importModal = page.getByTestId('import-collection-modal');
+ await importModal.waitFor({ state: 'visible' });
+
+ // upload the OpenAPI file with duplicate operation names
+ await page.setInputFiles('input[type="file"]', openApiFile);
+
+ // wait for the file processing to complete
+ await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
+
+ // verify that the collection location modal appears (OpenAPI files go directly to location modal)
+ const locationModal = page.getByTestId('import-collection-location-modal');
+ // verify the collection name is correctly parsed despite duplicate operation names
+ await expect(locationModal.getByText('Duplicate Test Collection')).toBeVisible();
+
+ // select a location
+ await page.locator('#collection-location').fill(await createTmpDir('duplicate-test'));
+ await page.getByRole('button', { name: 'Import', exact: true }).click();
+
+ // verify the collection was imported successfully
+ await expect(page.locator('#sidebar-collection-name').getByText('Duplicate Test Collection')).toBeVisible();
+
+ // configure the collection settings
+ await page.locator('#sidebar-collection-name').getByText('Duplicate Test Collection').click();
+ await page.getByLabel('Safe Mode').check();
+ await page.getByRole('button', { name: 'Save' }).click();
+
+ // verify that all 3 requests were imported correctly despite duplicate operation names
+ await expect(page.locator('#collection-duplicate-test-collection .collection-item-name')).toHaveCount(3);
+
+ // cleanup: close the collection
+ await page
+ .locator('.collection-name')
+ .filter({ has: page.locator('#sidebar-collection-name:has-text("Duplicate Test Collection")') })
+ .locator('.collection-actions')
+ .click();
+ await page.locator('.dropdown-item').getByText('Close').click();
+ await page.getByRole('button', { name: 'Close' }).click();
+ });
+});
diff --git a/tests/import/test-data/openapi-comprehensive.yaml b/tests/import/openapi/fixtures/openapi-comprehensive.yaml
similarity index 100%
rename from tests/import/test-data/openapi-comprehensive.yaml
rename to tests/import/openapi/fixtures/openapi-comprehensive.yaml
diff --git a/tests/import/openapi/fixtures/openapi-duplicate-operation-name.yaml b/tests/import/openapi/fixtures/openapi-duplicate-operation-name.yaml
new file mode 100644
index 000000000..8e95b9ed2
--- /dev/null
+++ b/tests/import/openapi/fixtures/openapi-duplicate-operation-name.yaml
@@ -0,0 +1,44 @@
+openapi: 3.0.0
+info:
+ title: Duplicate Test Collection
+ version: 1.0.0
+ description: Test collection for handling duplicate operation names
+servers:
+ - url: https://api.example.com
+ description: Example server
+paths:
+ /users:
+ get:
+ summary: 'Get Users'
+ description: 'Get all users'
+ operationId: getUsers
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ post:
+ summary: 'Get Users'
+ description: 'Create a new user (same summary as GET)'
+ operationId: createUser
+ responses:
+ '201':
+ description: Created
+ content:
+ application/json:
+ schema:
+ type: object
+ /products:
+ get:
+ summary: 'Get Users'
+ description: 'Get all products (same summary as users GET)'
+ operationId: getProducts
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
diff --git a/tests/import/test-data/openapi-invalid-version.yaml b/tests/import/openapi/fixtures/openapi-invalid-version.yaml
similarity index 100%
rename from tests/import/test-data/openapi-invalid-version.yaml
rename to tests/import/openapi/fixtures/openapi-invalid-version.yaml
diff --git a/tests/import/test-data/openapi-malformed.yaml b/tests/import/openapi/fixtures/openapi-malformed.yaml
similarity index 100%
rename from tests/import/test-data/openapi-malformed.yaml
rename to tests/import/openapi/fixtures/openapi-malformed.yaml
diff --git a/tests/import/test-data/openapi-missing-info.yaml b/tests/import/openapi/fixtures/openapi-missing-info.yaml
similarity index 100%
rename from tests/import/test-data/openapi-missing-info.yaml
rename to tests/import/openapi/fixtures/openapi-missing-info.yaml
diff --git a/tests/import/openapi/fixtures/openapi-newline-in-operation-name.yaml b/tests/import/openapi/fixtures/openapi-newline-in-operation-name.yaml
new file mode 100644
index 000000000..213933158
--- /dev/null
+++ b/tests/import/openapi/fixtures/openapi-newline-in-operation-name.yaml
@@ -0,0 +1,32 @@
+openapi: 3.0.0
+info:
+ title: Newline Test Collection
+ version: 1.0.0
+ description: Test collection for operation names with newlines
+servers:
+ - url: https://api.example.com
+ description: Example server
+paths:
+ /users:
+ get:
+ summary: "Get users\nwith newline"
+ description: 'This operation has newlines in the summary'
+ operationId: getUsersWithNewline
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ post:
+ summary: "Create user\n\nwith multiple\n\nnewlines"
+ description: 'This operation has multiple consecutive newlines'
+ operationId: createUserWithNewlines
+ responses:
+ '201':
+ description: Created
+ content:
+ application/json:
+ schema:
+ type: object
diff --git a/tests/import/test-data/openapi-simple.json b/tests/import/openapi/fixtures/openapi-simple.json
similarity index 100%
rename from tests/import/test-data/openapi-simple.json
rename to tests/import/openapi/fixtures/openapi-simple.json
diff --git a/tests/import/openapi/002-import-openapi-json.spec.ts b/tests/import/openapi/import-openapi-json.spec.ts
similarity index 90%
rename from tests/import/openapi/002-import-openapi-json.spec.ts
rename to tests/import/openapi/import-openapi-json.spec.ts
index 847d4c49b..750f438bd 100644
--- a/tests/import/openapi/002-import-openapi-json.spec.ts
+++ b/tests/import/openapi/import-openapi-json.spec.ts
@@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Import OpenAPI v3 JSON Collection', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Import simple OpenAPI v3 JSON successfully', async ({ page }) => {
- const openApiFile = path.join(testDataDir, 'openapi-simple.json');
+ const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-simple.json');
await page.getByRole('button', { name: 'Import Collection' }).click();
diff --git a/tests/import/openapi/001-import-openapi-yaml.spec.ts b/tests/import/openapi/import-openapi-yaml.spec.ts
similarity index 90%
rename from tests/import/openapi/001-import-openapi-yaml.spec.ts
rename to tests/import/openapi/import-openapi-yaml.spec.ts
index 0d705f1b1..8d490876c 100644
--- a/tests/import/openapi/001-import-openapi-yaml.spec.ts
+++ b/tests/import/openapi/import-openapi-yaml.spec.ts
@@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Import OpenAPI v3 YAML Collection', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Import comprehensive OpenAPI v3 YAML successfully', async ({ page }) => {
- const openApiFile = path.join(testDataDir, 'openapi-comprehensive.yaml');
+ const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-comprehensive.yaml');
await page.getByRole('button', { name: 'Import Collection' }).click();
diff --git a/tests/import/openapi/004-malformed-yaml.spec.ts b/tests/import/openapi/malformed-yaml.spec.ts
similarity index 90%
rename from tests/import/openapi/004-malformed-yaml.spec.ts
rename to tests/import/openapi/malformed-yaml.spec.ts
index 2f2479e9a..ec596d18a 100644
--- a/tests/import/openapi/004-malformed-yaml.spec.ts
+++ b/tests/import/openapi/malformed-yaml.spec.ts
@@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Invalid OpenAPI - Malformed YAML', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Handle malformed OpenAPI YAML structure', async ({ page }) => {
- const openApiFile = path.join(testDataDir, 'openapi-malformed.yaml');
+ const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-malformed.yaml');
await page.getByRole('button', { name: 'Import Collection' }).click();
diff --git a/tests/import/openapi/003-missing-info.spec.ts b/tests/import/openapi/missing-info.spec.ts
similarity index 89%
rename from tests/import/openapi/003-missing-info.spec.ts
rename to tests/import/openapi/missing-info.spec.ts
index aa8298dcd..bb8a672c1 100644
--- a/tests/import/openapi/003-missing-info.spec.ts
+++ b/tests/import/openapi/missing-info.spec.ts
@@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Invalid OpenAPI - Missing Info Section', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Handle OpenAPI specification missing required info section', async ({ page }) => {
- const openApiFile = path.join(testDataDir, 'openapi-missing-info.yaml');
+ const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-missing-info.yaml');
await page.getByRole('button', { name: 'Import Collection' }).click();
diff --git a/tests/import/openapi/operation-name-with-newlines-fix.spec.ts b/tests/import/openapi/operation-name-with-newlines-fix.spec.ts
new file mode 100644
index 000000000..2971ee4c9
--- /dev/null
+++ b/tests/import/openapi/operation-name-with-newlines-fix.spec.ts
@@ -0,0 +1,50 @@
+import { test, expect } from '../../../playwright';
+import * as path from 'path';
+
+test.describe('OpenAPI Newline Handling', () => {
+ test('should handle operation names with newlines', async ({ page, createTmpDir }) => {
+ const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-newline-in-operation-name.yaml');
+
+ // start the import process
+ await page.getByRole('button', { name: 'Import Collection' }).click();
+
+ // wait for the import collection modal to appear
+ const importModal = page.getByTestId('import-collection-modal');
+ await importModal.waitFor({ state: 'visible' });
+
+ // upload the OpenAPI file with problematic operation names
+ await page.setInputFiles('input[type="file"]', openApiFile);
+
+ // wait for the file processing to complete
+ await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
+
+ // verify that the collection location modal appears (OpenAPI files go directly to location modal)
+ const locationModal = page.getByTestId('import-collection-location-modal');
+ await expect(locationModal.getByText('Newline Test Collection')).toBeVisible();
+
+ // select a location
+ await page.locator('#collection-location').fill(await createTmpDir('newline-test'));
+ await page.getByRole('button', { name: 'Import', exact: true }).click();
+
+ // verify the collection was imported successfully
+ await expect(page.locator('#sidebar-collection-name').getByText('Newline Test Collection')).toBeVisible();
+
+ // configure the collection settings
+ await page.locator('#sidebar-collection-name').getByText('Newline Test Collection').click();
+ await page.getByLabel('Safe Mode').check();
+ await page.getByRole('button', { name: 'Save' }).click();
+
+ // verify that all requests were imported correctly despite newlines in operation names
+ // the parser should clean up the operation names and create valid request names
+ await expect(page.locator('#collection-newline-test-collection .collection-item-name')).toHaveCount(2);
+
+ // cleanup: close the collection
+ await page
+ .locator('.collection-name')
+ .filter({ has: page.locator('#sidebar-collection-name:has-text("Newline Test Collection")') })
+ .locator('.collection-actions')
+ .click();
+ await page.locator('.dropdown-item').getByText('Close').click();
+ await page.getByRole('button', { name: 'Close' }).click();
+ });
+});
diff --git a/tests/import/test-data/postman-invalid-missing-info.json b/tests/import/postman/fixtures/postman-invalid-missing-info.json
similarity index 100%
rename from tests/import/test-data/postman-invalid-missing-info.json
rename to tests/import/postman/fixtures/postman-invalid-missing-info.json
diff --git a/tests/import/test-data/postman-invalid-schema.json b/tests/import/postman/fixtures/postman-invalid-schema.json
similarity index 100%
rename from tests/import/test-data/postman-invalid-schema.json
rename to tests/import/postman/fixtures/postman-invalid-schema.json
diff --git a/tests/import/test-data/postman-malformed.json b/tests/import/postman/fixtures/postman-malformed.json
similarity index 100%
rename from tests/import/test-data/postman-malformed.json
rename to tests/import/postman/fixtures/postman-malformed.json
diff --git a/tests/import/test-data/postman-v20.json b/tests/import/postman/fixtures/postman-v20.json
similarity index 100%
rename from tests/import/test-data/postman-v20.json
rename to tests/import/postman/fixtures/postman-v20.json
diff --git a/tests/import/test-data/postman-v21.json b/tests/import/postman/fixtures/postman-v21.json
similarity index 100%
rename from tests/import/test-data/postman-v21.json
rename to tests/import/postman/fixtures/postman-v21.json
diff --git a/tests/import/postman/002-import-postman-v20.spec.ts b/tests/import/postman/import-postman-v20.spec.ts
similarity index 89%
rename from tests/import/postman/002-import-postman-v20.spec.ts
rename to tests/import/postman/import-postman-v20.spec.ts
index b6866fa3e..919da393f 100644
--- a/tests/import/postman/002-import-postman-v20.spec.ts
+++ b/tests/import/postman/import-postman-v20.spec.ts
@@ -2,18 +2,16 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Import Postman Collection v2.0', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Import Postman Collection v2.0 successfully', async ({ page }) => {
- const postmanFile = path.join(testDataDir, 'postman-v20.json');
+ const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-v20.json');
await page.getByRole('button', { name: 'Import Collection' }).click();
-
+
// Wait for import collection modal to be ready
const importModal = page.getByRole('dialog');
await importModal.waitFor({ state: 'visible' });
await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
-
+
await page.setInputFiles('input[type="file"]', postmanFile);
// Wait for the loader to disappear
@@ -22,10 +20,10 @@ test.describe('Import Postman Collection v2.0', () => {
// Verify that the Import Collection modal is displayed (for location selection)
const locationModal = page.getByRole('dialog');
await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
-
+
// Wait for collection to appear in the location modal
await expect(locationModal.getByText('Postman v2.0 Collection')).toBeVisible();
-
+
// Cleanup: close any open modals
await page.locator('[data-test-id="modal-close-button"]').click();
});
diff --git a/tests/import/postman/001-import-postman-v21.spec.ts b/tests/import/postman/import-postman-v21.spec.ts
similarity index 89%
rename from tests/import/postman/001-import-postman-v21.spec.ts
rename to tests/import/postman/import-postman-v21.spec.ts
index 08a7029df..ad7ccaca5 100644
--- a/tests/import/postman/001-import-postman-v21.spec.ts
+++ b/tests/import/postman/import-postman-v21.spec.ts
@@ -2,30 +2,28 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Import Postman Collection v2.1', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Import Postman Collection v2.1 successfully', async ({ page }) => {
- const postmanFile = path.join(testDataDir, 'postman-v21.json');
+ const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-v21.json');
await page.getByRole('button', { name: 'Import Collection' }).click();
-
+
// Wait for import collection modal to be ready
const importModal = page.getByRole('dialog');
await importModal.waitFor({ state: 'visible' });
await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
-
+
await page.setInputFiles('input[type="file"]', postmanFile);
// Wait for the loader to disappear
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
-
+
// Verify that the Import Collection modal is displayed (for location selection)
const locationModal = page.getByRole('dialog');
await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
-
+
// Wait for collection to appear in the location modal
await expect(locationModal.getByText('Postman v2.1 Collection')).toBeVisible();
-
+
// Cleanup: close any open modals
await page.locator('[data-test-id="modal-close-button"]').click();
});
diff --git a/tests/import/postman/006-invalid-json.spec.ts b/tests/import/postman/invalid-json.spec.ts
similarity index 86%
rename from tests/import/postman/006-invalid-json.spec.ts
rename to tests/import/postman/invalid-json.spec.ts
index ef1e881ca..fd214ced5 100644
--- a/tests/import/postman/006-invalid-json.spec.ts
+++ b/tests/import/postman/invalid-json.spec.ts
@@ -2,27 +2,25 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Invalid Postman Collection - Invalid JSON', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Handle invalid JSON syntax', async ({ page }) => {
- const postmanFile = path.join(testDataDir, 'postman-invalid-schema.json');
+ const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-invalid-schema.json');
await page.getByRole('button', { name: 'Import Collection' }).click();
-
+
// Wait for import collection modal to be ready
const importModal = page.getByRole('dialog');
await importModal.waitFor({ state: 'visible' });
await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
-
+
await page.setInputFiles('input[type="file"]', postmanFile);
-
+
// Wait for the loader to disappear
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
// Check for error message
const hasError = await page.getByText('Conversion failed').first().isVisible();
expect(hasError).toBe(true);
-
+
// Cleanup: close any open modals
await page.locator('[data-test-id="modal-close-button"]').click();
});
diff --git a/tests/import/postman/003-invalid-missing-info.spec.ts b/tests/import/postman/invalid-missing-info.spec.ts
similarity index 87%
rename from tests/import/postman/003-invalid-missing-info.spec.ts
rename to tests/import/postman/invalid-missing-info.spec.ts
index a68518388..90252f28a 100644
--- a/tests/import/postman/003-invalid-missing-info.spec.ts
+++ b/tests/import/postman/invalid-missing-info.spec.ts
@@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Invalid Postman Collection - Missing Info', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Handle Postman collection missing required info field', async ({ page }) => {
- const postmanFile = path.join(testDataDir, 'postman-invalid-missing-info.json');
+ const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-invalid-missing-info.json');
await page.getByRole('button', { name: 'Import Collection' }).click();
diff --git a/tests/import/postman/004-invalid-schema.spec.ts b/tests/import/postman/invalid-schema.spec.ts
similarity index 79%
rename from tests/import/postman/004-invalid-schema.spec.ts
rename to tests/import/postman/invalid-schema.spec.ts
index 34ddfa07b..aa9474d4b 100644
--- a/tests/import/postman/004-invalid-schema.spec.ts
+++ b/tests/import/postman/invalid-schema.spec.ts
@@ -2,27 +2,25 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Invalid Postman Collection - Invalid Schema', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Handle Postman collection with invalid schema version', async ({ page }) => {
- const postmanFile = path.join(testDataDir, 'postman-invalid-schema.json');
+ const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-invalid-schema.json');
await page.getByRole('button', { name: 'Import Collection' }).click();
-
+
// Wait for import collection modal to be ready
const importModal = page.getByRole('dialog');
await importModal.waitFor({ state: 'visible' });
await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
-
+
await page.setInputFiles('input[type="file"]', postmanFile);
-
+
// Wait for the loader to disappear
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
-
+
// Check for error message
- const hasError = await page.getByText('Conversion failed').isVisible();
+ const hasError = await page.getByText('Conversion failed').first().isVisible();
expect(hasError).toBe(true);
-
+
// Cleanup: close any open modals
await page.locator('[data-test-id="modal-close-button"]').click();
});
diff --git a/tests/import/postman/005-malformed-structure.spec.ts b/tests/import/postman/malformed-structure.spec.ts
similarity index 87%
rename from tests/import/postman/005-malformed-structure.spec.ts
rename to tests/import/postman/malformed-structure.spec.ts
index 2c39faf0d..e65109dd1 100644
--- a/tests/import/postman/005-malformed-structure.spec.ts
+++ b/tests/import/postman/malformed-structure.spec.ts
@@ -2,18 +2,16 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';
test.describe('Invalid Postman Collection - Malformed Structure', () => {
- const testDataDir = path.join(__dirname, '../test-data');
-
test('Handle malformed Postman collection structure', async ({ page }) => {
- const postmanFile = path.join(testDataDir, 'postman-malformed.json');
+ const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-malformed.json');
await page.getByRole('button', { name: 'Import Collection' }).click();
-
+
// Wait for import collection modal to be ready
const importModal = page.getByRole('dialog');
await importModal.waitFor({ state: 'visible' });
await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
-
+
await page.setInputFiles('input[type="file"]', postmanFile);
// Wait for the loader to disappear
@@ -22,7 +20,7 @@ test.describe('Invalid Postman Collection - Malformed Structure', () => {
// Check for error message
const hasError = await page.getByText('Import collection failed').first().isVisible();
expect(hasError).toBe(true);
-
+
// Cleanup: close any open modals
await page.locator('[data-test-id="modal-close-button"]').click();
});
diff --git a/tests/import/test-data/invalid-json.json b/tests/import/test-data/invalid-json.json
deleted file mode 100644
index f3ca33078..000000000
--- a/tests/import/test-data/invalid-json.json
+++ /dev/null
@@ -1 +0,0 @@
-{ "invalid": json syntax }
diff --git a/tests/interpolation/collection/bruno.json b/tests/interpolation/collection/bruno.json
new file mode 100644
index 000000000..f29e04549
--- /dev/null
+++ b/tests/interpolation/collection/bruno.json
@@ -0,0 +1,9 @@
+{
+ "version": "1",
+ "name": "interpolation",
+ "type": "collection",
+ "ignore": [
+ "node_modules",
+ ".git"
+ ]
+}
\ No newline at end of file
diff --git a/tests/interpolation/collection/echo-request-odata.bru b/tests/interpolation/collection/echo-request-odata.bru
new file mode 100644
index 000000000..f3cb79320
--- /dev/null
+++ b/tests/interpolation/collection/echo-request-odata.bru
@@ -0,0 +1,21 @@
+meta {
+ name: echo-request-odata
+ type: http
+ seq: 2
+}
+
+get {
+ url: http://localhost:8081/api/echo/path/Category(':CategoryID')/Item(:ItemId)/:xpath/Tags("tag test")
+ body: none
+ auth: inherit
+}
+
+params:path {
+ CategoryID: category123
+ ItemId: item456
+ xpath: foobar
+}
+
+settings {
+ encodeUrl: true
+}
diff --git a/tests/interpolation/collection/echo-request-url.bru b/tests/interpolation/collection/echo-request-url.bru
new file mode 100644
index 000000000..3b3b00754
--- /dev/null
+++ b/tests/interpolation/collection/echo-request-url.bru
@@ -0,0 +1,18 @@
+meta {
+ name: echo-request-url
+ type: http
+ seq: 1
+}
+
+get {
+ url: http://localhost:8081/api/echo/path/:path
+ auth: inherit
+}
+
+params:path {
+ path: some-data
+}
+
+settings {
+ encodeUrl: true
+}
diff --git a/tests/interpolation/init-user-data/collection-security.json b/tests/interpolation/init-user-data/collection-security.json
new file mode 100644
index 000000000..9369c1b91
--- /dev/null
+++ b/tests/interpolation/init-user-data/collection-security.json
@@ -0,0 +1,10 @@
+{
+ "collections": [
+ {
+ "path": "{{projectRoot}}/tests/interpolation/collection",
+ "securityConfig": {
+ "jsSandboxMode": "safe"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/interpolation/init-user-data/preferences.json b/tests/interpolation/init-user-data/preferences.json
new file mode 100644
index 000000000..38f64eadb
--- /dev/null
+++ b/tests/interpolation/init-user-data/preferences.json
@@ -0,0 +1,6 @@
+{
+ "maximized": true,
+ "lastOpenedCollections": [
+ "{{projectRoot}}/tests/interpolation/collection"
+ ]
+}
\ No newline at end of file
diff --git a/tests/interpolation/interpolate-request-url.spec.ts b/tests/interpolation/interpolate-request-url.spec.ts
new file mode 100644
index 000000000..061558a5c
--- /dev/null
+++ b/tests/interpolation/interpolate-request-url.spec.ts
@@ -0,0 +1,24 @@
+import { test, expect } from '../../playwright';
+
+test.describe.serial('URL Interpolation', () => {
+ test('Interpolate basic path params', async ({ pageWithUserData: page }) => {
+ await page.locator('#sidebar-collection-name').click();
+ await page.getByRole('complementary').getByText('echo-request-url').click();
+ await page.getByTestId('send-arrow-icon').click();
+
+ expect(page.getByTestId('response-status-code')).toHaveText(/200/);
+
+ const texts = await page.locator('div:nth-child(2) > .CodeMirror-scroll').allInnerTexts();
+ expect(texts.some(d => d.includes(`"url": "/path/some-data"`))).toBe(true);
+ });
+
+ test('Interpolate oData path params', async ({ pageWithUserData: page }) => {
+ await page.getByRole('complementary').getByText('echo-request-odata').click();
+ await page.getByTestId('send-arrow-icon').click();
+
+ expect(page.getByTestId('response-status-code')).toHaveText(/200/);
+
+ const texts = await page.locator('div:nth-child(2) > .CodeMirror-scroll').allInnerTexts();
+ expect(texts.some(d => d.includes(`"url": "/path/Category('category123')/Item(item456)/foobar/Tags(%22tag%20test%22)"`))).toBe(true);
+ });
+});
diff --git a/tests/import/test-data/empty-file.json b/tests/utils/pageUtils/actions.js
similarity index 100%
rename from tests/import/test-data/empty-file.json
rename to tests/utils/pageUtils/actions.js
diff --git a/tests/utils/pageUtils/index.js b/tests/utils/pageUtils/index.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/utils/pageUtils/navigation.js b/tests/utils/pageUtils/navigation.js
new file mode 100644
index 000000000..e69de29bb