diff --git a/eslint.config.js b/eslint.config.js
index fd712f667..30d59bc0f 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -7,14 +7,14 @@ const eslintPluginDiff = require('eslint-plugin-diff');
let stylistic;
const runESMImports = async () => {
- stylistic = await import('@stylistic/eslint-plugin').then(d => d.default);
+ stylistic = await import('@stylistic/eslint-plugin').then((d) => d.default);
};
module.exports = runESMImports().then(() => defineConfig([
{
plugins: {
'diff': fixupPluginRules(eslintPluginDiff),
- '@stylistic': stylistic,
+ '@stylistic': stylistic
},
languageOptions: {
parser: require('@typescript-eslint/parser'),
@@ -45,7 +45,7 @@ module.exports = runESMImports().then(() => defineConfig([
indent: 2,
quotes: 'single',
semi: true,
- jsx: true,
+ jsx: true
}).rules,
'@stylistic/comma-dangle': ['error', 'never'],
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],
@@ -53,7 +53,7 @@ module.exports = runESMImports().then(() => defineConfig([
'@stylistic/curly-newline': ['error', {
multiline: true,
minElements: 2,
- consistent: true,
+ consistent: true
}],
'@stylistic/function-paren-newline': ['error', 'never'],
'@stylistic/array-bracket-spacing': ['error', 'never'],
@@ -64,7 +64,7 @@ module.exports = runESMImports().then(() => defineConfig([
'@stylistic/semi-style': ['error', 'last'],
'@stylistic/max-len': ['off'],
'@stylistic/jsx-one-expression-per-line': ['off']
- },
+ }
},
{
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
diff --git a/package-lock.json b/package-lock.json
index 83491b236..93b148911 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30130,7 +30130,8 @@
"js-yaml": "^4.1.0",
"jscodeshift": "^17.3.0",
"lodash": "^4.17.21",
- "nanoid": "3.3.8"
+ "nanoid": "3.3.8",
+ "xml2js": "^0.6.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
diff --git a/packages/bruno-app/src/components/CollectionSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/index.js
index 85887ede3..333ed69b8 100644
--- a/packages/bruno-app/src/components/CollectionSettings/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/index.js
@@ -45,7 +45,7 @@ const CollectionSettings = ({ collection }) => {
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
const presets = get(collection, 'brunoConfig.presets', []);
- const hasPresets = presets && presets.requestUrl !== "";
+ const hasPresets = presets && presets.requestUrl !== '';
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
const proxyEnabled = proxyConfig.hostname ? true : false;
@@ -167,7 +167,7 @@ const CollectionSettings = ({ collection }) => {
setTab('presets')}>
Presets
- {hasPresets && }
+ {hasPresets && }
setTab('proxy')}>
Proxy
diff --git a/packages/bruno-app/src/components/Devtools/Console/index.js b/packages/bruno-app/src/components/Devtools/Console/index.js
index 5705eecb4..72bdf3487 100644
--- a/packages/bruno-app/src/components/Devtools/Console/index.js
+++ b/packages/bruno-app/src/components/Devtools/Console/index.js
@@ -13,7 +13,7 @@ import {
IconChevronDown,
IconTerminal2,
IconNetwork,
- IconDashboard,
+ IconDashboard
} from '@tabler/icons';
import {
closeConsole,
diff --git a/packages/bruno-app/src/components/Devtools/Performance/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Performance/StyledWrapper.js
index a71599627..7f6cb79a8 100644
--- a/packages/bruno-app/src/components/Devtools/Performance/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Devtools/Performance/StyledWrapper.js
@@ -5,7 +5,7 @@ const StyledWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
- background: ${props => props.theme.console.bg};
+ background: ${(props) => props.theme.console.bg};
}
.tab-content-area {
@@ -30,19 +30,19 @@ const StyledWrapper = styled.div`
.section-header {
margin-bottom: 20px;
padding-bottom: 12px;
- border-bottom: 1px solid ${props => props.theme.console.border};
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
h3 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
- color: ${props => props.theme.console.titleColor};
+ color: ${(props) => props.theme.console.titleColor};
}
p {
margin: 0;
font-size: 13px;
- color: ${props => props.theme.console.textMuted};
+ color: ${(props) => props.theme.console.textMuted};
}
}
@@ -53,7 +53,7 @@ const StyledWrapper = styled.div`
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
- color: ${props => props.theme.console.titleColor};
+ color: ${(props) => props.theme.console.titleColor};
}
}
@@ -65,8 +65,8 @@ const StyledWrapper = styled.div`
}
.resource-card {
- background: ${props => props.theme.console.headerBg};
- border: 1px solid ${props => props.theme.console.border};
+ background: ${(props) => props.theme.console.headerBg};
+ border: 1px solid ${(props) => props.theme.console.border};
border-radius: 4px;
padding: 8px;
}
@@ -76,7 +76,7 @@ const StyledWrapper = styled.div`
align-items: center;
gap: 6px;
margin-bottom: 6px;
- color: ${props => props.theme.console.titleColor};
+ color: ${(props) => props.theme.console.titleColor};
}
.resource-title {
@@ -87,13 +87,13 @@ const StyledWrapper = styled.div`
.resource-value {
font-size: 18px;
font-weight: 600;
- color: ${props => props.theme.console.titleColor};
+ color: ${(props) => props.theme.console.titleColor};
margin-bottom: 2px;
}
.resource-subtitle {
font-size: 11px;
- color: ${props => props.theme.console.buttonColor};
+ color: ${(props) => props.theme.console.buttonColor};
}
.resource-trend {
@@ -112,7 +112,7 @@ const StyledWrapper = styled.div`
}
&.stable {
- color: ${props => props.theme.console.buttonColor};
+ color: ${(props) => props.theme.console.buttonColor};
}
}
`;
diff --git a/packages/bruno-app/src/components/Devtools/Performance/index.js b/packages/bruno-app/src/components/Devtools/Performance/index.js
index 1de054b1d..fd83c56f9 100644
--- a/packages/bruno-app/src/components/Devtools/Performance/index.js
+++ b/packages/bruno-app/src/components/Devtools/Performance/index.js
@@ -6,13 +6,13 @@ import {
IconDatabase,
IconClock,
IconServer,
- IconChartLine,
+ IconChartLine
} from '@tabler/icons';
const Performance = () => {
- const { systemResources } = useSelector(state => state.performance);
+ const { systemResources } = useSelector((state) => state.performance);
- const formatBytes = bytes => {
+ const formatBytes = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
@@ -20,7 +20,7 @@ const Performance = () => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
- const formatUptime = seconds => {
+ const formatUptime = (seconds) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
diff --git a/packages/bruno-app/src/components/FolderSettings/Auth/index.js b/packages/bruno-app/src/components/FolderSettings/Auth/index.js
index 137cd223e..0bb8a1c37 100644
--- a/packages/bruno-app/src/components/FolderSettings/Auth/index.js
+++ b/packages/bruno-app/src/components/FolderSettings/Auth/index.js
@@ -17,7 +17,7 @@ import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth';
import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
- import { humanizeRequestAuthMode, getTreePathFromCollectionToItem } from 'utils/collections/index';
+import { humanizeRequestAuthMode, getTreePathFromCollectionToItem } from 'utils/collections/index';
const GrantTypeComponentMap = ({ collection, folder }) => {
const dispatch = useDispatch();
@@ -48,8 +48,6 @@ const Auth = ({ collection, folder }) => {
let request = get(folder, 'root.request', {});
const authMode = get(folder, 'root.request.auth.mode');
-
-
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
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 0d2222671..24055791f 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
@@ -18,7 +18,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const addButtonRef = useRef(null);
- const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector(state => state.globalEnvironments);
+ const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
let _collection = cloneDeep(collection);
diff --git a/packages/bruno-app/src/components/GlobalSearchModal/index.js b/packages/bruno-app/src/components/GlobalSearchModal/index.js
index a40d1aa8a..b813b0158 100644
--- a/packages/bruno-app/src/components/GlobalSearchModal/index.js
+++ b/packages/bruno-app/src/components/GlobalSearchModal/index.js
@@ -74,7 +74,7 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
if (isItemARequest(item)) {
// 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 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/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
index 9a42c885f..71d57d940 100644
--- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
@@ -251,7 +251,6 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
} catch (err) {}
}
-
function handleRevertChanges(event) {
event.stopPropagation();
dropdownTippyRef.current.hide();
@@ -263,12 +262,10 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
try {
const item = findItemInCollection(collection, currentTabUid);
if (item.draft) {
- dispatch(
- deleteRequestDraft({
- itemUid: item.uid,
- collectionUid: collection.uid
- })
- );
+ dispatch(deleteRequestDraft({
+ itemUid: item.uid,
+ collectionUid: collection.uid
+ }));
}
} catch (err) {}
}
@@ -340,7 +337,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
>
Clone Request
-
- Supports Bruno, Postman, Insomnia, and OpenAPI v3 formats
+ Supports Bruno, Postman, Insomnia, OpenAPI v3, and WSDL formats
diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js
index 1f92f0610..7ee1b8e02 100644
--- a/packages/bruno-app/src/providers/App/useIpcEvents.js
+++ b/packages/bruno-app/src/providers/App/useIpcEvents.js
@@ -146,7 +146,7 @@ const useIpcEvents = () => {
}));
});
- const removeSystemResourcesListener = ipcRenderer.on('main:filesync-system-resources', resourceData => {
+ const removeSystemResourcesListener = ipcRenderer.on('main:filesync-system-resources', (resourceData) => {
dispatch(updateSystemResources(resourceData));
});
diff --git a/packages/bruno-app/src/providers/ReduxStore/index.js b/packages/bruno-app/src/providers/ReduxStore/index.js
index d5abee753..b86305011 100644
--- a/packages/bruno-app/src/providers/ReduxStore/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/index.js
@@ -27,7 +27,7 @@ export const store = configureStore({
notifications: notificationsReducer,
globalEnvironments: globalEnvironmentsReducer,
logs: logsReducer,
- performance: performanceReducer,
+ performance: performanceReducer
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware)
});
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/performance.js b/packages/bruno-app/src/providers/ReduxStore/slices/performance.js
index efd7b01d3..20a95a8e4 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/performance.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/performance.js
@@ -6,8 +6,8 @@ const initialState = {
memory: 0,
pid: null,
uptime: 0,
- lastUpdated: null,
- },
+ lastUpdated: null
+ }
};
export const performanceSlice = createSlice({
@@ -18,10 +18,10 @@ export const performanceSlice = createSlice({
state.systemResources = {
...state.systemResources,
...action.payload,
- lastUpdated: new Date().toISOString(),
+ lastUpdated: new Date().toISOString()
};
- },
- },
+ }
+ }
});
export const { updateSystemResources } = performanceSlice.actions;
diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js
index 1411ffe03..b8a4bd19b 100644
--- a/packages/bruno-app/src/utils/codegenerator/har.js
+++ b/packages/bruno-app/src/utils/codegenerator/har.js
@@ -52,10 +52,10 @@ const createQuery = (queryParams = [], request) => {
value: param.value
}));
- if (request?.auth?.mode === 'apikey' &&
- request?.auth?.apikey?.placement === 'queryparams' &&
- request?.auth?.apikey?.key &&
- request?.auth?.apikey?.value) {
+ if (request?.auth?.mode === 'apikey'
+ && request?.auth?.apikey?.placement === 'queryparams'
+ && request?.auth?.apikey?.key
+ && request?.auth?.apikey?.value) {
params.push({
name: request.auth.apikey.key,
value: request.auth.apikey.value
diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
index a91fa706e..8f87c6824 100644
--- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
+++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
@@ -29,7 +29,7 @@ const COPY_SUCCESS_COLOR = '#22c55e';
export const COPY_SUCCESS_TIMEOUT = 1000;
-const getCopyButton = variableValue => {
+const getCopyButton = (variableValue) => {
const copyButton = document.createElement('button');
copyButton.className = 'copy-button';
@@ -64,7 +64,7 @@ const getCopyButton = variableValue => {
copyButton.style.opacity = '0.7';
});
- copyButton.addEventListener('click', e => {
+ copyButton.addEventListener('click', (e) => {
e.stopPropagation();
// Prevent clicking if showing success checkmark
@@ -91,7 +91,7 @@ const getCopyButton = variableValue => {
copyButton.classList.remove('copy-success');
}, COPY_SUCCESS_TIMEOUT);
})
- .catch(err => {
+ .catch((err) => {
console.error('Failed to copy to clipboard:', err.message);
});
});
diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js
index 0a2a161dc..f425d91a5 100644
--- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js
+++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js
@@ -237,7 +237,7 @@ describe('renderVarInfo', () => {
clipboardText = '';
Object.defineProperty(navigator, 'clipboard', {
value: {
- writeText: jest.fn(text => {
+ writeText: jest.fn((text) => {
if (text === 'cause-clipboard-error') {
return Promise.reject(new Error('Clipboard error'));
}
@@ -245,9 +245,9 @@ describe('renderVarInfo', () => {
clipboardText = text;
return Promise.resolve();
- }),
+ })
},
- configurable: true,
+ configurable: true
});
// mock console.error
@@ -283,7 +283,7 @@ describe('renderVarInfo', () => {
it('should correctly mask the variable value in the popup', () => {
const { descriptionDiv } = setupRender({
apiKey: 'test-value',
- maskedEnvVariables: ['apiKey'],
+ maskedEnvVariables: ['apiKey']
});
expect(descriptionDiv.textContent).toBe('*****');
diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js
index 6f00eca02..f9b11d816 100644
--- a/packages/bruno-app/src/utils/common/index.js
+++ b/packages/bruno-app/src/utils/common/index.js
@@ -54,7 +54,7 @@ export const safeStringifyJSON = (obj, indent = false) => {
export const prettifyJSON = (obj, spaces = 2) => {
try {
- const text = obj.replace(/\\"/g, '"').replace(/\\'/g, "'");
+ const text = obj.replace(/\\"/g, '"').replace(/\\'/g, '\'');
const placeholders = [];
const modifiedJson = text.replace(/"[^"]*?"|{{[^{}]+}}/g, (match) => {
diff --git a/packages/bruno-app/src/utils/common/masked-editor.js b/packages/bruno-app/src/utils/common/masked-editor.js
index c88402119..8b08d1698 100644
--- a/packages/bruno-app/src/utils/common/masked-editor.js
+++ b/packages/bruno-app/src/utils/common/masked-editor.js
@@ -102,7 +102,6 @@ export class MaskedEditor {
// Restore cursor state
this.restoreCursorState();
-
} finally {
this.isProcessing = false;
}
@@ -135,7 +134,6 @@ export class MaskedEditor {
// Restore cursor state
this.restoreCursorState();
-
} finally {
this.isProcessing = false;
}
@@ -331,15 +329,13 @@ export class MaskedEditor {
const maskedNode = document.createTextNode(this.maskChar.repeat(lineLength));
// Create mark with proper bounds checking
- const mark = this.editor.markText(
- { line, ch: 0 },
+ const mark = this.editor.markText({ line, ch: 0 },
{ line, ch: lineLength },
{
replacedWith: maskedNode,
handleMouseEvents: false,
className: 'masked-line'
- }
- );
+ });
// Store mark for cleanup
this.marks.add(mark);
@@ -355,7 +351,7 @@ export class MaskedEditor {
* Clear all marks with proper cleanup
*/
clearAllMarks() {
- this.marks.forEach(mark => {
+ this.marks.forEach((mark) => {
try {
mark.clear();
} catch (e) {
@@ -365,7 +361,7 @@ export class MaskedEditor {
this.marks.clear();
// Also clear any marks that might have been created outside our control
- this.editor.getAllMarks().forEach(mark => {
+ this.editor.getAllMarks().forEach((mark) => {
try {
mark.clear();
} catch (e) {
@@ -437,8 +433,8 @@ export function createMaskedEditor(editor, maskChar = '*') {
* Utility function to check if an editor supports masking
*/
export function supportsMasking(editor) {
- return editor &&
- typeof editor.getValue === 'function' &&
- typeof editor.markText === 'function' &&
- typeof editor.operation === 'function';
-}
\ No newline at end of file
+ return editor
+ && typeof editor.getValue === 'function'
+ && typeof editor.markText === 'function'
+ && typeof editor.operation === 'function';
+}
diff --git a/packages/bruno-app/src/utils/importers/wsdl-collection.js b/packages/bruno-app/src/utils/importers/wsdl-collection.js
new file mode 100644
index 000000000..d093454ca
--- /dev/null
+++ b/packages/bruno-app/src/utils/importers/wsdl-collection.js
@@ -0,0 +1,28 @@
+const isWSDLCollection = (data) => {
+ // Check if data is a string (WSDL content)
+ if (typeof data !== 'string') {
+ return false;
+ }
+
+ // Check for WSDL-specific XML elements
+ const wsdlIndicators = [
+ 'wsdl:definitions',
+ 'definitions',
+ 'wsdl:types',
+ 'wsdl:message',
+ 'wsdl:portType',
+ 'wsdl:binding',
+ 'wsdl:service'
+ ];
+
+ // Check if the content contains WSDL namespace or elements
+ const hasWSDLNamespace = data.includes('xmlns:wsdl=')
+ || data.includes('xmlns="http://schemas.xmlsoap.org/wsdl/"')
+ || data.includes('xmlns="http://www.w3.org/2001/XMLSchema"');
+
+ const hasWSDLElements = wsdlIndicators.some((indicator) => data.includes(indicator));
+
+ return hasWSDLNamespace || hasWSDLElements;
+};
+
+export { isWSDLCollection };
diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js
index 87b37ee20..268170a4b 100644
--- a/packages/bruno-app/src/utils/url/index.js
+++ b/packages/bruno-app/src/utils/url/index.js
@@ -34,7 +34,7 @@ export const parsePathParams = (url) => {
// Enhanced: also match :param inside parentheses and/or quotes
const foundParams = new Set();
- paths.forEach(segment => {
+ paths.forEach((segment) => {
// traditional path parameters
if (segment.startsWith(':')) {
const name = segment.slice(1);
@@ -65,7 +65,7 @@ export const parsePathParams = (url) => {
}
}
});
- return Array.from(foundParams).map(name => ({ name, value: '' }));
+ return Array.from(foundParams).map((name) => ({ name, value: '' }));
};
export const splitOnFirst = (str, char) => {
diff --git a/packages/bruno-app/src/utils/url/index.spec.js b/packages/bruno-app/src/utils/url/index.spec.js
index a8dd80a3f..f3ff35074 100644
--- a/packages/bruno-app/src/utils/url/index.spec.js
+++ b/packages/bruno-app/src/utils/url/index.spec.js
@@ -166,7 +166,6 @@ describe('Url Utils - parsePathParams', () => {
const params = parsePathParams('https://example.com/start/1:2:AHLS-HASD/form');
expect(params).toEqual([]);
});
-
});
describe('Url Utils - URN parsing', () => {
diff --git a/packages/bruno-cli/src/commands/import.js b/packages/bruno-cli/src/commands/import.js
index a7d4e8041..78701ea36 100644
--- a/packages/bruno-cli/src/commands/import.js
+++ b/packages/bruno-cli/src/commands/import.js
@@ -3,7 +3,7 @@ const path = require('path');
const chalk = require('chalk');
const jsyaml = require('js-yaml');
const axios = require('axios');
-const { openApiToBruno } = require('@usebruno/converters');
+const { openApiToBruno, wsdlToBruno } = require('@usebruno/converters');
const { exists, isDirectory, sanitizeName } = require('../utils/filesystem');
const { createCollectionFromBrunoObject } = require('../utils/collection');
@@ -15,7 +15,7 @@ const builder = (yargs) => {
.positional('type', {
describe: 'Type of collection to import',
type: 'string',
- choices: ['openapi']
+ choices: ['openapi', 'wsdl']
})
.option('source', {
alias: 's',
@@ -59,7 +59,9 @@ const builder = (yargs) => {
.example('$0 import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name "My API"')
.example('$0 import openapi -s api.yml -f ~/Desktop/my-collection.json -n "My API"')
.example('$0 import openapi --source api.yml --output ~/Desktop/my-collection --group-by path')
- .example('$0 import openapi -s api.yml -o ~/Desktop/my-collection -g tags');
+ .example('$0 import openapi -s api.yml -o ~/Desktop/my-collection -g tags')
+ .example('$0 import wsdl --source service.wsdl --output ~/Desktop/soap-collection --collection-name "SOAP Service"')
+ .example('$0 import wsdl -s https://example.com/service.wsdl -o ~/Desktop -n "Remote SOAP Service"');
};
const isUrl = (str) => {
@@ -139,12 +141,68 @@ const readOpenApiFile = async (source, options = {}) => {
}
};
+const readWSDLFile = async (source, options = {}) => {
+ try {
+ let content;
+
+ if (isUrl(source)) {
+ // Handle URL input
+ console.log(chalk.yellow(`Fetching WSDL from URL: ${source}`));
+ try {
+ const axiosOptions = {
+ timeout: 30000, // 30 second timeout
+ maxContentLength: 10 * 1024 * 1024,
+ validateStatus: (status) => status >= 200 && status < 300
+ };
+
+ // Skip SSL certificate validation if insecure flag is set
+ if (options.insecure) {
+ console.log(chalk.yellow('Warning: SSL certificate verification is disabled. Use with caution.'));
+ axiosOptions.httpsAgent = new (require('https')).Agent({ rejectUnauthorized: false });
+ }
+
+ const response = await axios.get(source, axiosOptions);
+ content = response.data;
+ } catch (error) {
+ if (error.code === 'ECONNABORTED') {
+ throw new Error('Request timed out. The server took too long to respond.');
+ } else if (error.code === 'CERT_HAS_EXPIRED' || error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT'
+ || error.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
+ throw new Error(`SSL Certificate error: ${error.code}. Try using --insecure if you trust this source.`);
+ } else if (error.response) {
+ throw new Error(`Failed to fetch from URL: ${error.response.status} ${error.response.statusText}`);
+ } else if (error.request) {
+ throw new Error(`No response received from server. Check the URL and your network connection.`);
+ } else {
+ throw new Error(`Error fetching URL: ${error.message}`);
+ }
+ }
+ } else {
+ // Handle file input
+ if (!await exists(source)) {
+ throw new Error(`File does not exist: ${source}`);
+ }
+ content = fs.readFileSync(source, 'utf8');
+ }
+
+ // WSDL files are XML, so we return the content as a string
+ if (typeof content === 'string') {
+ return content;
+ }
+
+ throw new Error('WSDL content must be a string');
+ } catch (error) {
+ // Let the specific error handling from above propagate
+ throw error;
+ }
+};
+
const handler = async (argv) => {
try {
const { type, source, output, outputFile, collectionName, insecure, groupBy } = argv;
- if (!type || type !== 'openapi') {
- console.error(chalk.red('Only OpenAPI import is supported currently'));
+ if (!type || !['openapi', 'wsdl'].includes(type)) {
+ console.error(chalk.red('Only OpenAPI and WSDL imports are supported currently'));
process.exit(1);
}
@@ -158,19 +216,37 @@ const handler = async (argv) => {
process.exit(1);
}
- console.log(chalk.yellow(`Reading OpenAPI specification from ${source}...`));
-
- const openApiSpec = await readOpenApiFile(source, { insecure });
-
- if (!openApiSpec) {
- console.error(chalk.red('Failed to parse OpenAPI specification'));
- process.exit(1);
- }
+ let brunoCollection;
- console.log(chalk.yellow('Converting OpenAPI specification to Bruno format...'));
-
- // Convert OpenAPI to Bruno format
- let brunoCollection = openApiToBruno(openApiSpec, { groupBy });
+ if (type === 'openapi') {
+ console.log(chalk.yellow(`Reading OpenAPI specification from ${source}...`));
+
+ const openApiSpec = await readOpenApiFile(source, { insecure });
+
+ if (!openApiSpec) {
+ console.error(chalk.red('Failed to parse OpenAPI specification'));
+ process.exit(1);
+ }
+
+ console.log(chalk.yellow('Converting OpenAPI specification to Bruno format...'));
+
+ // Convert OpenAPI to Bruno format
+ brunoCollection = openApiToBruno(openApiSpec, { groupBy });
+ } else if (type === 'wsdl') {
+ console.log(chalk.yellow(`Reading WSDL from ${source}...`));
+
+ const wsdlContent = await readWSDLFile(source, { insecure });
+
+ if (!wsdlContent) {
+ console.error(chalk.red('Failed to read WSDL file'));
+ process.exit(1);
+ }
+
+ console.log(chalk.yellow('Converting WSDL to Bruno format...'));
+
+ // Convert WSDL to Bruno format
+ brunoCollection = await wsdlToBruno(wsdlContent);
+ }
// Override collection name if provided
if (collectionName) {
@@ -235,5 +311,6 @@ module.exports = {
builder,
handler,
isUrl,
- readOpenApiFile
+ readOpenApiFile,
+ readWSDLFile
};
\ No newline at end of file
diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js
index caa6293cf..3ed8ba735 100644
--- a/packages/bruno-cli/src/runner/interpolate-vars.js
+++ b/packages/bruno-cli/src/runner/interpolate-vars.js
@@ -123,7 +123,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
// traditional path parameters
if (path.startsWith(':')) {
const paramName = path.slice(1);
- const existingPathParam = request.pathParams.find(param => param.name === paramName);
+ const existingPathParam = request.pathParams.find((param) => param.name === paramName);
if (!existingPathParam) {
return '/' + path;
}
diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js
index 6c6758ab2..89fc1dbd0 100644
--- a/packages/bruno-cli/src/runner/prepare-request.js
+++ b/packages/bruno-cli/src/runner/prepare-request.js
@@ -323,7 +323,7 @@ const prepareRequest = async (item = {}, collection = {}) => {
axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads
}
- const bodyFile = find(request.body.file, param => param.selected);
+ const bodyFile = find(request.body.file, (param) => param.selected);
if (bodyFile) {
let { filePath, contentType } = bodyFile;
diff --git a/packages/bruno-cli/tests/runner/prepare-request.spec.js b/packages/bruno-cli/tests/runner/prepare-request.spec.js
index 0d25fc3b3..14e5b5af4 100644
--- a/packages/bruno-cli/tests/runner/prepare-request.spec.js
+++ b/packages/bruno-cli/tests/runner/prepare-request.spec.js
@@ -559,7 +559,7 @@ describe('prepare-request: prepareRequest', () => {
selected: true
}]
}
- },
+ }
};
const result = await prepareRequest(item);
diff --git a/packages/bruno-converters/package.json b/packages/bruno-converters/package.json
index 2a2450ef0..77f8c45c1 100644
--- a/packages/bruno-converters/package.json
+++ b/packages/bruno-converters/package.json
@@ -23,7 +23,8 @@
"js-yaml": "^4.1.0",
"jscodeshift": "^17.3.0",
"lodash": "^4.17.21",
- "nanoid": "3.3.8"
+ "nanoid": "3.3.8",
+ "xml2js": "^0.6.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
diff --git a/packages/bruno-converters/readme.md b/packages/bruno-converters/readme.md
index 8f8df3f44..15bcece0b 100644
--- a/packages/bruno-converters/readme.md
+++ b/packages/bruno-converters/readme.md
@@ -44,6 +44,14 @@ const { openApiToBruno } = require('@usebruno/converters');
const brunoCollection = openApiToBruno(openApiSpecification);
```
+### Convert WSDL file to Bruno collection
+
+```javascript
+import { wsdlToBruno } from '@usebruno/converters';
+
+const brunoCollection = await wsdlToBruno(wsdlContent);
+```
+
## Example
```bash copy
@@ -76,3 +84,70 @@ const outputFilePath = path.resolve(__dirname, 'bruno-collection.json');
convertPostmanToBruno(inputFilePath, outputFilePath);
```
+
+## WSDL Import Features
+
+The WSDL importer supports the following features:
+
+- **Service Discovery**: Automatically extracts service endpoints from WSDL definitions
+- **Operation Mapping**: Converts WSDL operations to Bruno HTTP requests
+- **SOAP Envelope Generation**: Creates proper SOAP envelopes for each operation
+- **Header Configuration**: Sets up appropriate Content-Type and SOAPAction headers
+- **Environment Variables**: Creates environment variables for service base URLs
+- **Folder Organization**: Groups operations by port type for better organization
+
+### WSDL Import Example
+
+```javascript
+import { wsdlToBruno } from '@usebruno/converters';
+import fs from 'fs/promises';
+
+async function importWSDL() {
+ try {
+ // Read WSDL file
+ const wsdlContent = await fs.readFile('service.wsdl', 'utf8');
+
+ // Convert to Bruno collection
+ const brunoCollection = await wsdlToBruno(wsdlContent);
+
+ // Save Bruno collection
+ await fs.writeFile('soap-collection.json', JSON.stringify(brunoCollection, null, 2));
+
+ console.log('WSDL import successful!');
+ } catch (error) {
+ console.error('Error during WSDL import:', error);
+ }
+}
+
+importWSDL();
+```
+
+### CLI Usage
+
+You can also use the Bruno CLI to import WSDL files:
+
+```bash
+# Import WSDL file to a directory
+bruno import wsdl --source service.wsdl --output ~/Desktop/soap-collection --collection-name "SOAP Service"
+
+# Import WSDL from URL
+bruno import wsdl --source https://example.com/service.wsdl --output ~/Desktop --collection-name "Remote SOAP Service"
+
+# Import WSDL and save as JSON file
+bruno import wsdl --source service.wsdl --output-file ~/Desktop/soap-collection.json --collection-name "SOAP Service"
+```
+
+## Supported Formats
+
+- **Postman Collections** (v2.1)
+- **Insomnia Collections** (v4 and v5)
+- **OpenAPI Specifications** (v3.0)
+- **WSDL Files** (Web Services Description Language)
+
+## Dependencies
+
+- `lodash` - Utility functions
+- `nanoid` - UUID generation
+- `js-yaml` - YAML parsing
+- `xml2js` - XML parsing for WSDL
+- `@usebruno/schema` - Schema validation
diff --git a/packages/bruno-converters/src/index.js b/packages/bruno-converters/src/index.js
index d5b3d3a3b..1bcd54cea 100644
--- a/packages/bruno-converters/src/index.js
+++ b/packages/bruno-converters/src/index.js
@@ -3,4 +3,5 @@ export { default as postmanToBrunoEnvironment } from './postman/postman-env-to-b
export { default as brunoToPostman } from './postman/bruno-to-postman.js';
export { default as openApiToBruno } from './openapi/openapi-to-bruno.js';
export { default as insomniaToBruno } from './insomnia/insomnia-to-bruno.js';
+export { default as wsdlToBruno } from './wsdl/wsdl-to-bruno.js';
export { default as postmanTranslation } from './postman/postman-translations.js';
\ No newline at end of file
diff --git a/packages/bruno-converters/src/wsdl/wsdl-to-bruno.js b/packages/bruno-converters/src/wsdl/wsdl-to-bruno.js
new file mode 100644
index 000000000..659bebfc4
--- /dev/null
+++ b/packages/bruno-converters/src/wsdl/wsdl-to-bruno.js
@@ -0,0 +1,1133 @@
+// Custom UID generator for alphanumeric IDs (no hyphens)
+const generateUID = () => {
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ let result = '';
+ for (let i = 0; i < 21; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ return result;
+};
+
+import { get, each, filter } from 'lodash';
+import { collectionSchema } from '@usebruno/schema';
+
+// --- Inlined from src/common/index.js ---
+export const validateSchema = (collection = {}) => {
+ try {
+ collectionSchema.validateSync(collection);
+ return collection;
+ } catch (err) {
+ throw new Error('The Collection has an invalid schema: ' + err.message);
+ }
+};
+
+export const transformItemsInCollection = (collection) => {
+ const transformItems = (items = []) => {
+ each(items, (item) => {
+ if (['http', 'graphql'].includes(item.type)) {
+ item.type = `${item.type}-request`;
+ if (item.request.query) {
+ item.request.params = item.request.query.map((queryItem) => ({
+ ...queryItem,
+ type: 'query',
+ uid: queryItem.uid || generateUID()
+ }));
+ }
+ delete item.request.query;
+ let multipartFormData = get(item, 'request.body.multipartForm');
+ if (multipartFormData) {
+ each(multipartFormData, (form) => {
+ if (!form.type) {
+ form.type = 'text';
+ }
+ });
+ }
+ }
+ // Handle already transformed types
+ if (['http-request', 'graphql-request'].includes(item.type)) {
+ if (item.request.query) {
+ item.request.params = item.request.query.map((queryItem) => ({
+ ...queryItem,
+ type: 'query',
+ uid: queryItem.uid || generateUID()
+ }));
+ }
+ delete item.request.query;
+ let multipartFormData = get(item, 'request.body.multipartForm');
+ if (multipartFormData) {
+ each(multipartFormData, (form) => {
+ if (!form.type) {
+ form.type = 'text';
+ }
+ });
+ }
+ }
+ if (item.items && item.items.length) {
+ transformItems(item.items);
+ }
+ });
+ };
+ transformItems(collection.items);
+ return collection;
+};
+
+const isItemARequest = (item) => {
+ return ['http-request', 'graphql-request'].includes(item.type);
+};
+
+export const hydrateSeqInCollection = (collection) => {
+ const hydrateSeq = (items = []) => {
+ let index = 1;
+ each(items, (item) => {
+ if (isItemARequest(item) && !item.seq) {
+ item.seq = index;
+ index++;
+ }
+ if (item.items && item.items.length) {
+ hydrateSeq(item.items);
+ }
+ });
+ };
+ hydrateSeq(collection.items);
+ return collection;
+};
+// --- End inlined ---
+
+// Use a simple XML parser for Node.js environment
+const parseXML = (xmlString) => {
+ const parser = new (require('xml2js')).Parser({
+ explicitArray: false,
+ ignoreAttrs: false,
+ mergeAttrs: true,
+ xmlns: false
+ });
+
+ return new Promise((resolve, reject) => {
+ parser.parseString(xmlString, (err, result) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(result);
+ }
+ });
+ });
+};
+
+const addSuffixToDuplicateName = (item, index, allItems) => {
+ // Check if the request name already exist and if so add a number suffix
+ const nameSuffix = allItems.reduce((nameSuffix, otherItem, otherIndex) => {
+ if (otherItem.name === item.name && otherIndex < index) {
+ nameSuffix++;
+ }
+ return nameSuffix;
+ }, 0);
+ return nameSuffix !== 0 ? `${item.name}_${nameSuffix}` : item.name;
+};
+
+/**
+ * Enhanced WSDL Parser based on wizdler approach
+ */
+class WSDLParser {
+ constructor() {
+ this.types = new Map();
+ this.elements = new Map();
+ this.complexTypes = new Map();
+ this.simpleTypes = new Map();
+ this.messages = new Map();
+ this.portTypes = new Map();
+ this.bindings = new Map();
+ this.services = new Map();
+ this.namespaces = new Map();
+ }
+
+ /**
+ * Parse WSDL content and extract all components
+ */
+ async parse(wsdlContent) {
+ const result = await parseXML(wsdlContent);
+ const definitions = result['wsdl:definitions'] || result.definitions;
+
+ if (!definitions) {
+ throw new Error('No definitions found in WSDL');
+ }
+
+ // Extract namespaces
+ this.extractNamespaces(definitions);
+
+ // Parse types (XSD schemas)
+ if (definitions['wsdl:types'] || definitions.types) {
+ this.parseTypes(definitions['wsdl:types'] || definitions.types);
+ }
+
+ // Parse messages
+ this.parseMessages(definitions);
+
+ // Parse port types
+ this.parsePortTypes(definitions);
+
+ // Parse bindings
+ this.parseBindings(definitions);
+
+ // Parse services
+ this.parseServices(definitions);
+
+ return {
+ targetNamespace: definitions.targetNamespace || '',
+ name: definitions.name || 'WSDL Service',
+ types: this.types,
+ elements: this.elements,
+ complexTypes: this.complexTypes,
+ simpleTypes: this.simpleTypes,
+ messages: this.messages,
+ portTypes: this.portTypes,
+ bindings: this.bindings,
+ services: this.services,
+ namespaces: this.namespaces
+ };
+ }
+
+ /**
+ * Extract all namespaces from WSDL
+ */
+ extractNamespaces(definitions) {
+ // Extract from xmlns attributes
+ for (const [key, value] of Object.entries(definitions)) {
+ if (key.startsWith('xmlns:')) {
+ const prefix = key.substring(6);
+ this.namespaces.set(prefix, value);
+ } else if (key === 'xmlns') {
+ this.namespaces.set('', value);
+ }
+ }
+ }
+
+ /**
+ * Parse WSDL types section (XSD schemas)
+ */
+ parseTypes(typesNode) {
+ if (!typesNode) return;
+
+ const schemas = this.getArray(typesNode['xsd:schema'] || typesNode.schema);
+
+ for (const schema of schemas) {
+ const targetNamespace = schema.targetNamespace || '';
+
+ // Parse complex types FIRST (so they can be referenced by elements)
+ const complexTypes = this.getArray(schema['xsd:complexType'] || schema.complexType);
+ for (const complexType of complexTypes) {
+ this.parseComplexType(complexType, targetNamespace);
+ }
+
+ // Parse simple types
+ const simpleTypes = this.getArray(schema['xsd:simpleType'] || schema.simpleType);
+ for (const simpleType of simpleTypes) {
+ this.parseSimpleType(simpleType, targetNamespace);
+ }
+
+ // Parse elements LAST (so they can reference complex types)
+ const elements = this.getArray(schema['xsd:element'] || schema.element);
+ for (const element of elements) {
+ this.parseElement(element, targetNamespace);
+ }
+ }
+ }
+
+ /**
+ * Parse an XSD element
+ */
+ parseElement(element, namespace) {
+ const key = `${namespace}:${element.name}`;
+ const parsedElement = {
+ name: element.name,
+ namespace: namespace,
+ type: element.type,
+ minOccurs: element.minOccurs,
+ maxOccurs: element.maxOccurs,
+ nillable: element.nillable,
+ form: element.form,
+ attributes: [],
+ elements: []
+ };
+
+ // Handle inline complex type (recursively parse children)
+ if (element['xsd:complexType'] || element.complexType) {
+ const complexType = element['xsd:complexType'] || element.complexType;
+ // Recursively parse sequence/choice/all children as elements
+ if (complexType['xsd:sequence'] || complexType.sequence) {
+ const sequence = complexType['xsd:sequence'] || complexType.sequence;
+ const children = this.getArray(sequence['xsd:element'] || sequence.element);
+ for (const child of children) {
+ // Recursively parse child element
+ parsedElement.elements.push(this.parseElementInline(child, namespace));
+ }
+ }
+ if (complexType['xsd:choice'] || complexType.choice) {
+ const choice = complexType['xsd:choice'] || complexType.choice;
+ const children = this.getArray(choice['xsd:element'] || choice.element);
+ for (const child of children) {
+ parsedElement.elements.push(this.parseElementInline(child, namespace));
+ }
+ }
+ if (complexType['xsd:all'] || complexType.all) {
+ const all = complexType['xsd:all'] || complexType.all;
+ const children = this.getArray(all['xsd:element'] || all.element);
+ for (const child of children) {
+ parsedElement.elements.push(this.parseElementInline(child, namespace));
+ }
+ }
+ // Parse attributes
+ if (complexType['xsd:attribute'] || complexType.attribute) {
+ const attributes = this.getArray(complexType['xsd:attribute'] || complexType.attribute);
+ for (const attr of attributes) {
+ parsedElement.attributes.push({
+ name: attr.name,
+ type: attr.type,
+ use: attr.use,
+ default: attr.default,
+ fixed: attr.fixed,
+ form: attr.form
+ });
+ }
+ }
+ }
+
+ // Handle inline simple type
+ if (element['xsd:simpleType'] || element.simpleType) {
+ const simpleType = element['xsd:simpleType'] || element.simpleType;
+ parsedElement.simpleType = this.parseSimpleTypeContent(simpleType);
+ }
+
+ // Handle referenced complex type - resolve it immediately
+ if (element.type && !element['xsd:complexType'] && !element['xsd:simpleType']) {
+ const typeName = element.type.replace(/^.*:/, '');
+ const complexType = this.findComplexTypeByName(typeName, namespace);
+ if (complexType) {
+ parsedElement.elements = complexType.elements || [];
+ parsedElement.attributes = complexType.attributes || [];
+ }
+ }
+
+ this.elements.set(key, parsedElement);
+ return parsedElement; // for inline recursion
+ }
+
+ /**
+ * Helper for parsing inline child elements (does not add to elements map)
+ */
+ parseElementInline(element, namespace) {
+ const parsedElement = {
+ name: element.name,
+ namespace: namespace,
+ type: element.type,
+ minOccurs: element.minOccurs,
+ maxOccurs: element.maxOccurs,
+ nillable: element.nillable,
+ form: element.form,
+ attributes: [],
+ elements: []
+ };
+ // Inline complex type
+ if (element['xsd:complexType'] || element.complexType) {
+ const complexType = element['xsd:complexType'] || element.complexType;
+ if (complexType['xsd:sequence'] || complexType.sequence) {
+ const sequence = complexType['xsd:sequence'] || complexType.sequence;
+ const children = this.getArray(sequence['xsd:element'] || sequence.element);
+ for (const child of children) {
+ parsedElement.elements.push(this.parseElementInline(child, namespace));
+ }
+ }
+ if (complexType['xsd:choice'] || complexType.choice) {
+ const choice = complexType['xsd:choice'] || complexType.choice;
+ const children = this.getArray(choice['xsd:element'] || choice.element);
+ for (const child of children) {
+ parsedElement.elements.push(this.parseElementInline(child, namespace));
+ }
+ }
+ if (complexType['xsd:all'] || complexType.all) {
+ const all = complexType['xsd:all'] || complexType.all;
+ const children = this.getArray(all['xsd:element'] || all.element);
+ for (const child of children) {
+ parsedElement.elements.push(this.parseElementInline(child, namespace));
+ }
+ }
+ // Parse attributes
+ if (complexType['xsd:attribute'] || complexType.attribute) {
+ const attributes = this.getArray(complexType['xsd:attribute'] || complexType.attribute);
+ for (const attr of attributes) {
+ parsedElement.attributes.push({
+ name: attr.name,
+ type: attr.type,
+ use: attr.use,
+ default: attr.default,
+ fixed: attr.fixed,
+ form: attr.form
+ });
+ }
+ }
+ }
+ // Inline simple type
+ if (element['xsd:simpleType'] || element.simpleType) {
+ const simpleType = element['xsd:simpleType'] || element.simpleType;
+ parsedElement.simpleType = this.parseSimpleTypeContent(simpleType);
+ }
+ // Referenced complex type
+ if (element.type && !element['xsd:complexType'] && !element['xsd:simpleType']) {
+ const typeName = element.type.replace(/^.*:/, '');
+ const complexType = this.findComplexTypeByName(typeName, namespace);
+ if (complexType) {
+ parsedElement.elements = complexType.elements || [];
+ parsedElement.attributes = complexType.attributes || [];
+ }
+ }
+ return parsedElement;
+ }
+
+ /**
+ * Find complex type by name and namespace
+ */
+ findComplexTypeByName(typeName, namespace) {
+ // Try with namespace
+ const key = `${namespace}:${typeName}`;
+ if (this.complexTypes.has(key)) {
+ return this.complexTypes.get(key);
+ }
+
+ // Try without namespace
+ for (const [key, complexType] of this.complexTypes) {
+ if (complexType.name === typeName) {
+ return complexType;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Parse an XSD complex type
+ */
+ parseComplexType(complexType, namespace) {
+ const key = `${namespace}:${complexType.name}`;
+ const parsedComplexType = {
+ name: complexType.name,
+ namespace: namespace,
+ attributes: [],
+ elements: [],
+ mixed: complexType.mixed,
+ abstract: complexType.abstract
+ };
+
+ this.parseComplexTypeContent(complexType, parsedComplexType);
+ this.complexTypes.set(key, parsedComplexType);
+ }
+
+ /**
+ * Parse complex type content (sequence, choice, all, attributes)
+ */
+ parseComplexTypeContent(complexType, target) {
+ // Parse sequence
+ if (complexType['xsd:sequence'] || complexType.sequence) {
+ const sequence = complexType['xsd:sequence'] || complexType.sequence;
+ this.parseSequence(sequence, target);
+ }
+
+ // Parse choice
+ if (complexType['xsd:choice'] || complexType.choice) {
+ const choice = complexType['xsd:choice'] || complexType.choice;
+ this.parseChoice(choice, target);
+ }
+
+ // Parse all
+ if (complexType['xsd:all'] || complexType.all) {
+ const all = complexType['xsd:all'] || complexType.all;
+ this.parseAll(all, target);
+ }
+
+ // Parse attributes
+ if (complexType['xsd:attribute'] || complexType.attribute) {
+ const attributes = this.getArray(complexType['xsd:attribute'] || complexType.attribute);
+ for (const attr of attributes) {
+ target.attributes.push({
+ name: attr.name,
+ type: attr.type,
+ use: attr.use,
+ default: attr.default,
+ fixed: attr.fixed,
+ form: attr.form
+ });
+ }
+ }
+
+ // Handle simple content with extension
+ if (complexType['xsd:simpleContent'] || complexType.simpleContent) {
+ const simpleContent = complexType['xsd:simpleContent'] || complexType.simpleContent;
+ if (simpleContent['xsd:extension'] || simpleContent.extension) {
+ const extension = simpleContent['xsd:extension'] || simpleContent.extension;
+ target.baseType = extension.base;
+
+ // Parse attributes from extension
+ if (extension['xsd:attribute'] || extension.attribute) {
+ const attributes = this.getArray(extension['xsd:attribute'] || extension.attribute);
+ for (const attr of attributes) {
+ target.attributes.push({
+ name: attr.name,
+ type: attr.type,
+ use: attr.use,
+ default: attr.default,
+ fixed: attr.fixed,
+ form: attr.form
+ });
+ }
+ }
+ }
+ }
+
+ // Handle complex content with extension
+ if (complexType['xsd:complexContent'] || complexType.complexContent) {
+ const complexContent = complexType['xsd:complexContent'] || complexType.complexContent;
+ if (complexContent['xsd:extension'] || complexContent.extension) {
+ const extension = complexContent['xsd:extension'] || complexContent.extension;
+ target.baseType = extension.base;
+
+ // Parse content from extension
+ this.parseComplexTypeContent(extension, target);
+ }
+ }
+ }
+
+ /**
+ * Parse sequence content
+ */
+ parseSequence(sequence, target) {
+ const elements = this.getArray(sequence['xsd:element'] || sequence.element);
+ for (const element of elements) {
+ // Use parseElementInline to properly handle inline complex types and attributes
+ const parsedElement = this.parseElementInline(element, target.namespace || '');
+ target.elements.push(parsedElement);
+ }
+ }
+
+ /**
+ * Parse choice content
+ */
+ parseChoice(choice, target) {
+ const elements = this.getArray(choice['xsd:element'] || choice.element);
+ for (const element of elements) {
+ // Use parseElementInline to properly handle inline complex types and attributes
+ const parsedElement = this.parseElementInline(element, target.namespace || '');
+ parsedElement.choice = true;
+ target.elements.push(parsedElement);
+ }
+ }
+
+ /**
+ * Parse all content
+ */
+ parseAll(all, target) {
+ const elements = this.getArray(all['xsd:element'] || all.element);
+ for (const element of elements) {
+ // Use parseElementInline to properly handle inline complex types and attributes
+ const parsedElement = this.parseElementInline(element, target.namespace || '');
+ parsedElement.all = true;
+ target.elements.push(parsedElement);
+ }
+ }
+
+ /**
+ * Parse simple type
+ */
+ parseSimpleType(simpleType, namespace) {
+ const key = `${namespace}:${simpleType.name}`;
+ const parsedSimpleType = {
+ name: simpleType.name,
+ namespace: namespace,
+ ...this.parseSimpleTypeContent(simpleType)
+ };
+ this.simpleTypes.set(key, parsedSimpleType);
+ }
+
+ /**
+ * Parse simple type content
+ */
+ parseSimpleTypeContent(simpleType) {
+ if (simpleType['xsd:restriction'] || simpleType.restriction) {
+ const restriction = simpleType['xsd:restriction'] || simpleType.restriction;
+ return {
+ base: restriction.base,
+ enumeration: this.getArray(restriction['xsd:enumeration'] || restriction.enumeration),
+ pattern: restriction['xsd:pattern'] || restriction.pattern,
+ minLength: restriction['xsd:minLength'] || restriction.minLength,
+ maxLength: restriction['xsd:maxLength'] || restriction.maxLength
+ };
+ }
+ return {};
+ }
+
+ /**
+ * Parse WSDL messages
+ */
+ parseMessages(definitions) {
+ const messages = this.getArray(definitions['wsdl:message'] || definitions.message);
+ for (const message of messages) {
+ const parts = this.getArray(message['wsdl:part'] || message.part);
+ this.messages.set(message.name, {
+ name: message.name,
+ parts: parts.map((part) => ({
+ name: part.name,
+ type: part.type,
+ element: part.element
+ }))
+ });
+ }
+ }
+
+ /**
+ * Parse WSDL port types
+ */
+ parsePortTypes(definitions) {
+ const portTypes = this.getArray(definitions['wsdl:portType'] || definitions.portType);
+ for (const portType of portTypes) {
+ const operations = this.getArray(portType['wsdl:operation'] || portType.operation);
+ this.portTypes.set(portType.name, {
+ name: portType.name,
+ operations: operations.map((op) => ({
+ name: op.name,
+ input: op['wsdl:input'] || op.input,
+ output: op['wsdl:output'] || op.output,
+ fault: this.getArray(op['wsdl:fault'] || op.fault)
+ }))
+ });
+ }
+ }
+
+ /**
+ * Parse WSDL bindings
+ */
+ parseBindings(definitions) {
+ const bindings = this.getArray(definitions['wsdl:binding'] || definitions.binding);
+ for (const binding of bindings) {
+ const operations = this.getArray(binding['wsdl:operation'] || binding.operation);
+ this.bindings.set(binding.name, {
+ name: binding.name,
+ type: binding.type,
+ operations: operations.map((op) => {
+ // Robustly extract soapAction from any soap:operation child element
+ let soapAction = '';
+ for (const key of Object.keys(op)) {
+ if (key.endsWith(':operation')) {
+ const soapOp = op[key];
+ if (Array.isArray(soapOp)) {
+ if (soapOp[0] && soapOp[0].soapAction) {
+ soapAction = soapOp[0].soapAction;
+ break;
+ }
+ } else if (soapOp && soapOp.soapAction) {
+ soapAction = soapOp.soapAction;
+ break;
+ }
+ }
+ }
+ return {
+ name: op.name,
+ input: op['wsdl:input'] || op.input,
+ output: op['wsdl:output'] || op.output,
+ fault: this.getArray(op['wsdl:fault'] || op.fault),
+ soapAction: soapAction
+ };
+ })
+ });
+ }
+ }
+
+ /**
+ * Parse WSDL services
+ */
+ parseServices(definitions) {
+ const services = this.getArray(definitions['wsdl:service'] || definitions.service);
+ for (const service of services) {
+ const ports = this.getArray(service['wsdl:port'] || service.port);
+ this.services.set(service.name, {
+ name: service.name,
+ ports: ports.map((port) => ({
+ name: port.name,
+ binding: port.binding,
+ address: this.extractAddress(port)
+ }))
+ });
+ }
+ }
+
+ /**
+ * Extract service address from port
+ */
+ extractAddress(port) {
+ // Try different address formats
+ const address = port['soap:address'] || port['wsdl:address'] || port.address;
+ if (address && address.location) {
+ return address.location;
+ }
+ return '';
+ }
+
+ /**
+ * Helper to ensure array
+ */
+ getArray(item) {
+ if (!item) return [];
+ return Array.isArray(item) ? item : [item];
+ }
+}
+
+/**
+ * Enhanced XML Sample Generator based on wizdler approach
+ */
+class XMLSampleGenerator {
+ constructor(wsdlData) {
+ this.wsdlData = wsdlData;
+ this.visitedTypes = new Set();
+ }
+
+ /**
+ * Generate XML sample for an element
+ */
+ generateSample(elementName, namespace = '') {
+ const element = this.findElement(elementName, namespace);
+ if (!element) {
+ return ``;
+ }
+
+ return this.generateElementSample(element, 0);
+ }
+
+ /**
+ * Find element by name and namespace
+ */
+ findElement(elementName, namespace) {
+ // Try with namespace
+ if (namespace) {
+ const key = `${namespace}:${elementName}`;
+ if (this.wsdlData.elements.has(key)) {
+ return this.wsdlData.elements.get(key);
+ }
+ }
+
+ // Try without namespace
+ for (const [key, element] of this.wsdlData.elements) {
+ if (element.name === elementName) {
+ return element;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Generate sample for an element
+ */
+ generateElementSample(element) {
+ let xml = '';
+
+ // Add comments for optional/repetition elements
+ const minOccurs = parseInt(element.minOccurs) || 1;
+ const maxOccurs = element.maxOccurs || '1';
+
+ if (minOccurs === 0) {
+ xml += ``;
+ }
+
+ if (maxOccurs === 'unbounded' || (typeof maxOccurs === 'number' && maxOccurs > 1)) {
+ xml += ``;
+ }
+
+ // Generate attributes
+ const attributes = this.generateAttributes(element);
+
+ // Generate element content
+ if (this.isSimpleType(element)) {
+ xml += `<${element.name}${attributes}>${this.getSampleValue(element)}${element.name}>`;
+ } else {
+ xml += `<${element.name}${attributes}>`;
+ xml += this.generateComplexContent(element);
+ xml += `${element.name}>`;
+ }
+
+ return xml;
+ }
+
+ /**
+ * Recursively collect all attributes from a complex type and its base types
+ */
+ collectAllAttributes(complexType) {
+ let attributes = [];
+ if (complexType && complexType.attributes) {
+ attributes = attributes.concat(complexType.attributes);
+ }
+ // Recursively collect from base type if present
+ if (complexType && complexType.baseType) {
+ const baseTypeName = complexType.baseType.replace(/^.*:/, '');
+ const baseType = this.findComplexType(baseTypeName);
+ if (baseType) {
+ attributes = attributes.concat(this.collectAllAttributes(baseType));
+ }
+ }
+ return attributes;
+ }
+
+ /**
+ * Generate attributes string
+ */
+ generateAttributes(element) {
+ let attributes = [];
+
+ // Add attributes from the element itself
+ if (element.attributes && element.attributes.length > 0) {
+ attributes = attributes.concat(element.attributes);
+ }
+
+ // Add attributes from the referenced complex type (if any, recursively)
+ if (element.type) {
+ const complexType = this.findComplexType(element.type);
+ if (complexType) {
+ const allTypeAttrs = this.collectAllAttributes(complexType);
+ // Avoid duplicates by attribute name
+ const existingNames = new Set(attributes.map((a) => a.name));
+ for (const attr of allTypeAttrs) {
+ if (!existingNames.has(attr.name)) {
+ attributes.push(attr);
+ }
+ }
+ }
+ }
+
+ if (attributes.length > 0) {
+ return ' ' + attributes.map((attr) => `${attr.name}="?"`).join(' ');
+ }
+ return '';
+ }
+
+ /**
+ * Check if element is simple type
+ */
+ isSimpleType(element) {
+ if (element.simpleType) return true;
+
+ const type = element.type;
+ if (!type) return false;
+
+ // Check if it's a built-in simple type
+ const simpleTypes = [
+ 'string', 'int', 'integer', 'long', 'short', 'byte', 'boolean', 'float', 'double', 'decimal',
+ 'date', 'dateTime', 'time', 'duration', 'gYear', 'gYearMonth', 'gMonth', 'gMonthDay', 'gDay',
+ 'hexBinary', 'base64Binary', 'anyURI', 'QName', 'NOTATION', 'normalizedString', 'token',
+ 'language', 'Name', 'NCName', 'ID', 'IDREF', 'IDREFS', 'ENTITY', 'ENTITIES', 'NMTOKEN', 'NMTOKENS'
+ ];
+
+ const typeName = type.replace(/^.*:/, '');
+ return simpleTypes.includes(typeName);
+ }
+
+ /**
+ * Get sample value for simple type
+ */
+ getSampleValue(element) {
+ if (element.simpleType && element.simpleType.enumeration && element.simpleType.enumeration.length > 0) {
+ return element.simpleType.enumeration[0].value || '?';
+ }
+
+ const type = element.type;
+ if (!type) return '?';
+
+ const typeName = type.replace(/^.*:/, '');
+
+ switch (typeName) {
+ case 'string': return 'string';
+ case 'int':
+ case 'integer':
+ case 'long':
+ case 'short':
+ case 'byte': return '0';
+ case 'boolean': return 'true';
+ case 'float':
+ case 'double':
+ case 'decimal': return '0.0';
+ case 'date': return '2024-01-01';
+ case 'dateTime': return '2024-01-01T00:00:00Z';
+ case 'time': return '00:00:00';
+ default: return '?';
+ }
+ }
+
+ /**
+ * Generate complex content
+ */
+ generateComplexContent(element) {
+ let xml = '';
+
+ // Handle inline complex type (elements already parsed)
+ if (element.elements && element.elements.length > 0) {
+ for (const child of element.elements) {
+ xml += this.generateElementSample(child);
+ }
+ }
+
+ // Handle referenced complex type - this is the key fix
+ if (element.type) {
+ const complexType = this.findComplexType(element.type);
+ if (complexType) {
+ xml += this.generateComplexTypeSample(complexType);
+ } else {
+ // If we can't find the complex type, try to find it as an element
+ const elementType = this.findElement(element.type.replace(/^.*:/, ''), '');
+ if (elementType) {
+ xml += this.generateElementSample(elementType);
+ }
+ }
+ }
+
+ return xml;
+ }
+
+ /**
+ * Find complex type by name
+ */
+ findComplexType(typeName) {
+ const cleanTypeName = typeName.replace(/^.*:/, '');
+
+ // First try exact match
+ for (const [key, complexType] of this.wsdlData.complexTypes) {
+ if (complexType.name === cleanTypeName) {
+ return complexType;
+ }
+ }
+
+ // Try with namespace prefix
+ for (const [key, complexType] of this.wsdlData.complexTypes) {
+ if (key.endsWith(`:${cleanTypeName}`) || key === cleanTypeName) {
+ return complexType;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Generate sample for complex type
+ */
+ generateComplexTypeSample(complexType) {
+ if (this.visitedTypes.has(complexType.name)) {
+ return '';
+ }
+
+ this.visitedTypes.add(complexType.name);
+ let xml = '';
+
+ if (complexType.elements && complexType.elements.length > 0) {
+ for (const element of complexType.elements) {
+ xml += this.generateElementSample(element);
+ }
+ }
+
+ this.visitedTypes.delete(complexType.name);
+ return xml;
+ }
+
+ /**
+ * Get repetition text
+ */
+ getRepetitionText(minOccurs, maxOccurs) {
+ if (minOccurs === 0 && maxOccurs === 'unbounded') {
+ return '0 or more repetitions';
+ } else if (minOccurs === 1 && maxOccurs === 'unbounded') {
+ return '1 or more repetitions';
+ } else if (typeof maxOccurs === 'number') {
+ return `${minOccurs} to ${maxOccurs} repetitions:`;
+ } else {
+ return '0 or more repetitions';
+ }
+ }
+}
+
+/**
+ * Generate SOAP envelope with example payload
+ */
+const generateSOAPEnvelope = (operation, wsdlData) => {
+ const inputMessage = operation.input?.message || '';
+ const inputMessageName = typeof inputMessage === 'string' && inputMessage.includes(':') ? inputMessage.split(':')[1] : inputMessage;
+
+ // Find the message definition
+ const message = wsdlData.messages.get(inputMessageName);
+ if (!message || !message.parts || message.parts.length === 0) {
+ return '';
+ }
+
+ const part = message.parts[0];
+ const elementName = part.element || part.type || '';
+
+ if (!elementName) {
+ return '';
+ }
+
+ // Extract element name and namespace
+ let name, namespace;
+ if (elementName.includes(':')) {
+ [namespace, name] = elementName.split(':');
+ } else {
+ name = elementName;
+ namespace = '';
+ }
+
+ // Generate XML sample
+ const generator = new XMLSampleGenerator(wsdlData);
+ const xmlSample = generator.generateSample(name, namespace);
+
+ return `${xmlSample}`;
+};
+
+/**
+ * Transform WSDL operation to Bruno request item
+ */
+const transformWSDLOperation = (operation, wsdlData, serviceLocation, index, allOperations, bindingOperation = null) => {
+ // Create a temporary object with the name property for duplicate checking
+ const tempItem = { name: operation.name };
+ const name = addSuffixToDuplicateName(tempItem, index, allOperations);
+ const soapEnvelope = generateSOAPEnvelope(operation, wsdlData);
+
+ // Use soapAction from binding operation if available, otherwise fallback to constructed value
+ let soapAction = '';
+ if (bindingOperation && bindingOperation.soapAction) {
+ soapAction = bindingOperation.soapAction;
+ } else {
+ // Fallback to constructed value
+ soapAction = `"${wsdlData.targetNamespace || ''}${operation.name}"`;
+ }
+
+ const brunoRequestItem = {
+ uid: generateUID(),
+ name,
+ type: 'http-request',
+ request: {
+ url: serviceLocation || '',
+ method: 'POST',
+ auth: {
+ mode: 'none',
+ basic: null,
+ bearer: null,
+ digest: null
+ },
+ headers: [
+ {
+ uid: generateUID(),
+ name: 'Content-Type',
+ value: 'text/xml; charset=utf-8',
+ description: '',
+ enabled: true
+ },
+ {
+ uid: generateUID(),
+ name: 'SOAPAction',
+ value: soapAction,
+ description: '',
+ enabled: true
+ }
+ ],
+ params: [],
+ body: {
+ mode: 'xml',
+ json: null,
+ text: null,
+ xml: soapEnvelope,
+ formUrlEncoded: [],
+ multipartForm: []
+ },
+ script: {
+ res: null
+ }
+ }
+ };
+
+ return brunoRequestItem;
+};
+
+/**
+ * Parse WSDL collection and transform to Bruno format
+ */
+const parseWSDLCollection = (wsdlData) => {
+ const collection = {
+ uid: generateUID(),
+ version: '1',
+ name: wsdlData.name,
+ items: []
+ };
+
+ // Flatten the structure to avoid duplicate folder names
+ // Group operations by service and port, but create a single folder per service
+ for (const [serviceName, service] of wsdlData.services) {
+ const serviceFolder = {
+ uid: generateUID(),
+ name: serviceName,
+ type: 'folder',
+ items: []
+ };
+
+ // Collect all operations from all ports in this service
+ const allOperations = [];
+
+ for (const port of service.ports) {
+ // Find operations for this port
+ const bindingName = port.binding && typeof port.binding === 'string' && port.binding.includes(':') ? port.binding.split(':')[1] : port.binding;
+ const binding = wsdlData.bindings.get(bindingName);
+
+ if (binding) {
+ const bindingType = binding.type && typeof binding.type === 'string' && binding.type.includes(':') ? binding.type.split(':')[1] : binding.type;
+ const portType = wsdlData.portTypes.get(bindingType);
+
+ if (portType) {
+ for (const portTypeOp of portType.operations) {
+ // Find the corresponding binding operation by name
+ const bindingOp = binding.operations.find((bop) => bop.name === portTypeOp.name);
+ if (bindingOp) {
+ const request = transformWSDLOperation(portTypeOp, wsdlData, port.address, allOperations.length, binding.operations, bindingOp);
+ allOperations.push(request);
+ }
+ }
+ }
+ }
+ }
+
+ // Add all operations directly to the service folder
+ serviceFolder.items = allOperations;
+
+ if (serviceFolder.items.length > 0) {
+ collection.items.push(serviceFolder);
+ }
+ }
+
+ return collection;
+};
+
+/**
+ * Convert WSDL content to Bruno collection
+ */
+export const wsdlToBruno = async (wsdlContent) => {
+ try {
+ if (typeof wsdlContent !== 'string') {
+ throw new Error('WSDL content must be a string');
+ }
+
+ // Parse WSDL using enhanced parser
+ const parser = new WSDLParser();
+ const wsdlData = await parser.parse(wsdlContent);
+
+ const collection = parseWSDLCollection(wsdlData);
+ const transformedCollection = transformItemsInCollection(collection);
+ const hydratedCollection = hydrateSeqInCollection(transformedCollection);
+ const validatedCollection = validateSchema(hydratedCollection);
+
+ return validatedCollection;
+ } catch (err) {
+ console.error(err);
+ throw new Error('Import WSDL collection failed: ' + err.message);
+ }
+};
+
+export { WSDLParser, XMLSampleGenerator };
+export default wsdlToBruno;
diff --git a/packages/bruno-converters/tests/wsdl/wsdl-to-bruno.spec.js b/packages/bruno-converters/tests/wsdl/wsdl-to-bruno.spec.js
new file mode 100644
index 000000000..32e83778a
--- /dev/null
+++ b/packages/bruno-converters/tests/wsdl/wsdl-to-bruno.spec.js
@@ -0,0 +1,16 @@
+import { describe, it, expect } from '@jest/globals';
+import wsdlToBruno from '../../src/wsdl/wsdl-to-bruno.js';
+
+describe('wsdl-to-bruno', () => {
+ it('should throw error for non-string input', async () => {
+ await expect(wsdlToBruno({})).rejects.toThrow('WSDL content must be a string');
+ });
+
+ it('should throw error for empty string input', async () => {
+ await expect(wsdlToBruno('')).rejects.toThrow('Import WSDL collection failed');
+ });
+
+ it('should throw error for invalid XML', async () => {
+ await expect(wsdlToBruno('xml')).rejects.toThrow('Import WSDL collection failed');
+ });
+});
diff --git a/packages/bruno-electron/src/app/system-monitor.js b/packages/bruno-electron/src/app/system-monitor.js
index 48fc27852..6d5771c15 100644
--- a/packages/bruno-electron/src/app/system-monitor.js
+++ b/packages/bruno-electron/src/app/system-monitor.js
@@ -43,7 +43,7 @@ class SystemMonitor {
memory: stats.memory || 0,
pid: pid,
uptime: uptime,
- timestamp: new Date().toISOString(),
+ timestamp: new Date().toISOString()
};
win.webContents.send('main:filesync-system-resources', systemResources);
@@ -56,7 +56,7 @@ class SystemMonitor {
memory: process.memoryUsage().rss,
pid: process.pid,
uptime: (Date.now() - this.startTime) / 1000,
- timestamp: new Date().toISOString(),
+ timestamp: new Date().toISOString()
};
win.webContents.send('main:filesync-system-resources', fallbackStats);
diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
index 92decafec..72a9265ac 100644
--- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js
+++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
@@ -91,14 +91,14 @@ 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) {}
@@ -148,7 +148,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
// traditional path parameters
if (path.startsWith(':')) {
const paramName = path.slice(1);
- const existingPathParam = request.pathParams.find(param => param.name === paramName);
+ const existingPathParam = request.pathParams.find((param) => param.name === paramName);
if (!existingPathParam) {
return '/' + path;
}
diff --git a/packages/bruno-electron/tests/network/interpolate-vars.spec.js b/packages/bruno-electron/tests/network/interpolate-vars.spec.js
index 0c1e4f178..f2f1ebe7d 100644
--- a/packages/bruno-electron/tests/network/interpolate-vars.spec.js
+++ b/packages/bruno-electron/tests/network/interpolate-vars.spec.js
@@ -163,19 +163,19 @@ describe('interpolate-vars: interpolateVars', () => {
{
type: 'path',
name: 'CategoryID',
- value: 'foobar',
+ value: 'foobar'
},
{
type: 'path',
name: 'ItemId',
- value: 1,
+ value: 1
},
{
type: 'path',
name: 'xpath',
- value: 'foobar',
- },
- ],
+ value: 'foobar'
+ }
+ ]
};
const result = interpolateVars(request, null, null, null);
diff --git a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js
index c6a043995..949f4f377 100644
--- a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js
+++ b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js
@@ -90,5 +90,4 @@ describe('prepareGqlIntrospectionRequest', () => {
expect(result.headers['X-API-Key']).toBe('{{process.env.MISSING_VAR}}');
});
-
-});
\ No newline at end of file
+});
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js
index c541c0375..f3938ba38 100644
--- a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js
@@ -90,7 +90,7 @@ const addBrunoRequestShimToContext = (vm, req) => {
let getBody = vm.newFunction('getBody', function (options) {
return marshallToVm(req.getBody(vm.dump(options)), vm);
});
-
+
vm.setProp(reqObject, 'getBody', getBody);
getBody.dispose();
diff --git a/packages/bruno-requests/src/ws/ws-client.js b/packages/bruno-requests/src/ws/ws-client.js
index a22d40318..4a3ec24a9 100644
--- a/packages/bruno-requests/src/ws/ws-client.js
+++ b/packages/bruno-requests/src/ws/ws-client.js
@@ -22,7 +22,6 @@ const safeParseJSON = (jsonString, context = 'JSON string') => {
}
};
-
class WsClient {
messageQueues = {};
activeConnections = new Map();
diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js
index 569642541..e65c60661 100644
--- a/packages/bruno-tests/src/index.js
+++ b/packages/bruno-tests/src/index.js
@@ -54,4 +54,4 @@ server.on('upgrade', wsRouter);
server.listen(port, function () {
console.log(`Testbench started on port: ${port}`);
-});
\ No newline at end of file
+});
diff --git a/packages/bruno-tests/src/ws/index.js b/packages/bruno-tests/src/ws/index.js
index 918042b00..d8ad980ed 100644
--- a/packages/bruno-tests/src/ws/index.js
+++ b/packages/bruno-tests/src/ws/index.js
@@ -8,12 +8,12 @@ const wss = new ws.Server({
noServer: true,
handleProtocols: (protocols, request) => {
if (request.url == '/ws/sub-proto') {
- if (protocols.has("soap")) {
- return 'soap'
+ if (protocols.has('soap')) {
+ return 'soap';
}
- return false
+ return false;
}
- return false
+ return false;
}
});
@@ -55,20 +55,18 @@ const wsRouter = (request, socket, head) => {
}
if (request.url == '/ws/sub-proto') {
- const subproto = request.headers["sec-websocket-protocol"] || request.headers["Sec-WebSocket-Protocol"]
- if (subproto != "soap") {
- const message = "Unsupported WebSocket subprotocol"
- socket.write(
- 'HTTP/1.1 400 Bad Request\r\n' +
- 'Content-Type: text/plain\r\n' +
- `Content-Length: ${Buffer.byteLength(message)}\r\n` +
- 'Connection: close\r\n' +
- '\r\n' +
- message
- );
+ const subproto = request.headers['sec-websocket-protocol'] || request.headers['Sec-WebSocket-Protocol'];
+ if (subproto != 'soap') {
+ const message = 'Unsupported WebSocket subprotocol';
+ socket.write('HTTP/1.1 400 Bad Request\r\n'
+ + 'Content-Type: text/plain\r\n'
+ + `Content-Length: ${Buffer.byteLength(message)}\r\n`
+ + 'Connection: close\r\n'
+ + '\r\n'
+ + message);
socket.destroy();
socket.removeListener('error', onSocketError);
- return
+ return;
}
}
diff --git a/tests/import/file-types/file-input-acceptance.spec.ts b/tests/import/file-types/file-input-acceptance.spec.ts
index 903f00d68..3718ffd13 100644
--- a/tests/import/file-types/file-input-acceptance.spec.ts
+++ b/tests/import/file-types/file-input-acceptance.spec.ts
@@ -1,19 +1,19 @@
import { test, expect } from '../../../playwright';
test.describe('File Input Acceptance', () => {
- test('File input accepts expected file types', async ({ page }) => {
+ test('File input accepts expected file types', async ({ page }) => {
await page.getByRole('button', { name: 'Import Collection' }).click();
-
+
// Check that file input exists (even if hidden)
const fileInput = page.locator('input[type="file"]');
await expect(fileInput).toBeAttached();
-
+
// Verify it accepts the expected file types
const acceptValue = await fileInput.getAttribute('accept');
expect(acceptValue).toContain('.json');
expect(acceptValue).toContain('.yaml');
expect(acceptValue).toContain('.yml');
-
+
// Cleanup: close any open modals
await page.locator('[data-test-id="modal-close-button"]').click();
});
diff --git a/tests/import/wsdl/fixtures/wsdl-bruno.json b/tests/import/wsdl/fixtures/wsdl-bruno.json
new file mode 100644
index 000000000..bf77f0730
--- /dev/null
+++ b/tests/import/wsdl/fixtures/wsdl-bruno.json
@@ -0,0 +1,102 @@
+{
+ "uid": "TestServiceCollection",
+ "version": "1",
+ "name": "TestWSDLServiceJSON",
+ "items": [
+ {
+ "uid": "UserServiceFolder",
+ "name": "UserService",
+ "type": "folder",
+ "items": [
+ {
+ "uid": "GetUserRequest",
+ "name": "GetUser",
+ "type": "http-request",
+ "seq": 1,
+ "request": {
+ "url": "http://example.com/soap/userservice",
+ "method": "POST",
+ "auth": {
+ "mode": "none",
+ "basic": null,
+ "bearer": null,
+ "digest": null
+ },
+ "headers": [
+ {
+ "uid": "ContentTypeHeader",
+ "name": "Content-Type",
+ "value": "text/xml; charset=utf-8",
+ "description": "",
+ "enabled": true
+ },
+ {
+ "uid": "SOAPActionHeader",
+ "name": "SOAPAction",
+ "value": "http://example.com/testservice/GetUser",
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "params": [],
+ "body": {
+ "mode": "xml",
+ "json": null,
+ "text": null,
+ "xml": "stringtrue",
+ "formUrlEncoded": [],
+ "multipartForm": []
+ },
+ "script": {
+ "res": null
+ }
+ }
+ },
+ {
+ "uid": "CreateUserRequest",
+ "name": "CreateUser",
+ "type": "http-request",
+ "seq": 2,
+ "request": {
+ "url": "http://example.com/soap/userservice",
+ "method": "POST",
+ "auth": {
+ "mode": "none",
+ "basic": null,
+ "bearer": null,
+ "digest": null
+ },
+ "headers": [
+ {
+ "uid": "ContentTypeHeader2",
+ "name": "Content-Type",
+ "value": "text/xml; charset=utf-8",
+ "description": "",
+ "enabled": true
+ },
+ {
+ "uid": "SOAPActionHeader2",
+ "name": "SOAPAction",
+ "value": "http://example.com/testservice/CreateUser",
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "params": [],
+ "body": {
+ "mode": "xml",
+ "json": null,
+ "text": null,
+ "xml": "stringstringstring",
+ "formUrlEncoded": [],
+ "multipartForm": []
+ },
+ "script": {
+ "res": null
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/tests/import/wsdl/fixtures/wsdl.xml b/tests/import/wsdl/fixtures/wsdl.xml
new file mode 100644
index 000000000..e46a2d922
--- /dev/null
+++ b/tests/import/wsdl/fixtures/wsdl.xml
@@ -0,0 +1,126 @@
+
+
+
+ Test WSDL for Bruno import testing
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ User management service port type
+
+
+ Retrieve user information by ID
+
+
+
+
+
+ Create a new user
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ User management web service
+
+
+
+
+
+
diff --git a/tests/import/wsdl/import-wsdl.spec.ts b/tests/import/wsdl/import-wsdl.spec.ts
new file mode 100644
index 000000000..f59b92d6f
--- /dev/null
+++ b/tests/import/wsdl/import-wsdl.spec.ts
@@ -0,0 +1,127 @@
+import { test, expect } from '../../../playwright';
+import * as path from 'path';
+import { closeAllCollections, openCollectionAndAcceptSandbox } from '../../utils/page/actions';
+
+test.describe('Import WSDL Collection', () => {
+ const testDataDir = path.join(__dirname, 'fixtures');
+
+ test.afterEach(async ({ page }) => {
+ await closeAllCollections(page);
+ });
+
+ test('Import WSDL XML file as Bruno collection', async ({ page, createTmpDir }) => {
+ const wsdlFile = path.join(testDataDir, 'wsdl.xml');
+
+ await test.step('Open import collection modal', async () => {
+ 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 test.step('Choose WSDL XML file', async () => {
+ await page.setInputFiles('input[type="file"]', wsdlFile);
+
+ // Wait for the loader to disappear
+ await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
+ });
+
+ await test.step('Select the location for the collection and submit to import', async () => {
+ // Verify that the location selection modal is displayed to import the collection
+ 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('TestWSDLServiceXML')).toBeVisible();
+
+ // select a location
+ await page.locator('#collection-location').fill(await createTmpDir('wsdl-xml-test'));
+ await page.getByRole('button', { name: 'Import', exact: true }).click();
+ });
+
+ await test.step('Verify that the collection was imported successfully', async () => {
+ // verify the collection was imported successfully
+ await expect(page.locator('#sidebar-collection-name').getByText('TestWSDLServiceXML')).toBeVisible();
+
+ // open the collection and accept the sandbox modal
+ await openCollectionAndAcceptSandbox(page, 'TestWSDLServiceXML', 'safe');
+
+ // verify that all requests were imported correctly
+ await expect(page.locator('#collection-testwsdlservicexml .collection-item-name')).toHaveCount(1);
+ });
+
+ await test.step('Verify that folders and requests were imported correctly', async () => {
+ await expect(page.locator('#collection-testwsdlservicexml .collection-item-name').getByText('UserService')).toBeVisible();
+ // open the user service folder
+ await page.locator('#collection-testwsdlservicexml .collection-item-name').getByText('UserService').click();
+
+ await expect(page.locator('#collection-testwsdlservicexml .collection-item-name').getByText('GetUser')).toBeVisible();
+ await expect(page.locator('#collection-testwsdlservicexml .collection-item-name').getByText('CreateUser')).toBeVisible();
+ });
+
+ await test.step('Verify the GetUser request is imported correctly', async () => {
+ await page.locator('#collection-testwsdlservicexml .collection-item-name').getByText('GetUser').click();
+ await expect(page.locator('.request-tab.active').getByText('GetUser')).toBeVisible();
+ await expect(page.locator('#request-url').getByText('http://example.com/soap/userservice')).toBeVisible();
+ });
+ });
+
+ test('Import WSDL JSON file as Bruno collection', async ({ page, createTmpDir }) => {
+ const wsdlFile = path.join(testDataDir, 'wsdl-bruno.json');
+
+ await test.step('Open import collection modal', async () => {
+ 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 test.step('Choose WSDL JSON file', async () => {
+ await page.setInputFiles('input[type="file"]', wsdlFile);
+
+ // Wait for the loader to disappear
+ await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
+ });
+
+ await test.step('Select the location for the collection and submit to import', async () => {
+ // Verify that the location selection modal is displayed to import the collection
+ 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('TestWSDLServiceJSON')).toBeVisible();
+
+ // select a location
+ await page.locator('#collection-location').fill(await createTmpDir('wsdl-json-test'));
+ await page.getByRole('button', { name: 'Import', exact: true }).click();
+ });
+
+ await test.step('Verify that the collection was imported successfully', async () => {
+ // verify the collection was imported successfully
+ await expect(page.locator('#sidebar-collection-name').getByText('TestWSDLServiceJSON')).toBeVisible();
+
+ // open the collection and accept the sandbox modal
+ await openCollectionAndAcceptSandbox(page, 'TestWSDLServiceJSON', 'safe');
+
+ // verify that all requests were imported correctly
+ await expect(page.locator('#collection-testwsdlservicejson .collection-item-name')).toHaveCount(1);
+ });
+
+ await test.step('Verify that folders and requests were imported correctly', async () => {
+ await expect(page.locator('#collection-testwsdlservicejson .collection-item-name').getByText('UserService')).toBeVisible();
+ // open the user service folder
+ await page.locator('#collection-testwsdlservicejson .collection-item-name').getByText('UserService').click();
+
+ await expect(page.locator('#collection-testwsdlservicejson .collection-item-name').getByText('GetUser')).toBeVisible();
+ await expect(page.locator('#collection-testwsdlservicejson .collection-item-name').getByText('CreateUser')).toBeVisible();
+ });
+
+ await test.step('Verify the CreateUser request is imported correctly', async () => {
+ await page.locator('#collection-testwsdlservicejson .collection-item-name').getByText('CreateUser').click();
+ await expect(page.locator('.request-tab.active').getByText('CreateUser')).toBeVisible();
+ await expect(page.locator('#request-url').getByText('http://example.com/soap/userservice')).toBeVisible();
+ });
+ });
+});
diff --git a/tests/runner/collection-run-report/collection-run-report.spec.ts b/tests/runner/collection-run-report/collection-run-report.spec.ts
index f7c3aabef..42005729c 100644
--- a/tests/runner/collection-run-report/collection-run-report.spec.ts
+++ b/tests/runner/collection-run-report/collection-run-report.spec.ts
@@ -17,7 +17,7 @@ function normalizeJunitReport(xmlContent: string): string {
test.describe('Collection Run Report Tests', () => {
const collectionPath = path.join(__dirname, 'collection');
-
+
test('CLI: Run collection and generate JUnit report', async ({ createTmpDir }) => {
const outputDir = await createTmpDir('junit-report');
const junitOutputPath = path.join(outputDir, 'cli-report.xml');