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