Import WSDL to collection (#5015)

* Import WSDL to bruno collection

* feat(wsdl-import): remove unused code and minor refactor

---------

Co-authored-by: Bijin Bruno <bijin@usebruno.com>
This commit is contained in:
Anton
2025-10-25 11:50:18 +02:00
committed by GitHub
parent 77bb8f40fe
commit a538b27f24
44 changed files with 1826 additions and 135 deletions

View File

@@ -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}"],

3
package-lock.json generated
View File

@@ -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",

View File

@@ -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 }) => {
</div>
<div className={getTabClassname('presets')} role="tab" onClick={() => setTab('presets')}>
Presets
{hasPresets && <StatusDot />}
{hasPresets && <StatusDot />}
</div>
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
Proxy

View File

@@ -13,7 +13,7 @@ import {
IconChevronDown,
IconTerminal2,
IconNetwork,
IconDashboard,
IconDashboard
} from '@tabler/icons';
import {
closeConsole,

View File

@@ -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};
}
}
`;

View File

@@ -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);

View File

@@ -17,7 +17,7 @@ import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth';
import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
import { humanizeRequestAuthMode, getTreePathFromCollectionToItem } from 'utils/collections/index';
import { 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;

View File

@@ -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);

View File

@@ -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 doesnt 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));

View File

@@ -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
</button>
<button
<button
className="dropdown-item w-full"
onClick={handleRevertChanges}
disabled={!currentTabItem?.draft}

View File

@@ -5,14 +5,21 @@ import Modal from 'components/Modal';
import jsyaml from 'js-yaml';
import { postmanToBruno, isPostmanCollection } from 'utils/importers/postman-collection';
import { convertInsomniaToBruno, isInsomniaCollection } from 'utils/importers/insomnia-collection';
import { isOpenApiSpec, convertOpenapiToBruno } from 'utils/importers/openapi-collection';
import { convertOpenapiToBruno, isOpenApiSpec } from 'utils/importers/openapi-collection';
import { isWSDLCollection } from 'utils/importers/wsdl-collection';
import { processBrunoCollection } from 'utils/importers/bruno-collection';
import { wsdlToBruno } from '@usebruno/converters';
import ImportSettings from 'components/Sidebar/ImportSettings';
import FullscreenLoader from './FullscreenLoader/index';
const convertFileToObject = async (file) => {
const text = await file.text();
// Handle WSDL files - return as plain text
if (file.name.endsWith('.wsdl') || file.type === 'text/xml' || file.type === 'application/xml') {
return text;
}
try {
if (file.type === 'application/json' || file.name.endsWith('.json')) {
return JSON.parse(text);
@@ -79,8 +86,9 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
}
let collection;
if (isPostmanCollection(data)) {
if (isWSDLCollection(data)) {
collection = await wsdlToBruno(data);
} else if (isPostmanCollection(data)) {
collection = await postmanToBruno(data);
} else if (isInsomniaCollection(data)) {
collection = convertInsomniaToBruno(data);
@@ -120,7 +128,17 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
return <FullscreenLoader isLoading={isLoading} />;
}
const acceptedFileTypes = ['.json', '.yaml', '.yml', 'application/json', 'application/yaml', 'application/x-yaml'];
const acceptedFileTypes = [
'.json',
'.yaml',
'.yml',
'.wsdl',
'application/json',
'application/yaml',
'application/x-yaml',
'text/xml',
'application/xml'
];
if (showImportSettings) {
return (
@@ -170,7 +188,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
</button>
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Supports Bruno, Postman, Insomnia, and OpenAPI v3 formats
Supports Bruno, Postman, Insomnia, OpenAPI v3, and WSDL formats
</p>
</div>
</div>

View File

@@ -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));
});

View File

@@ -27,7 +27,7 @@ export const store = configureStore({
notifications: notificationsReducer,
globalEnvironments: globalEnvironmentsReducer,
logs: logsReducer,
performance: performanceReducer,
performance: performanceReducer
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware)
});

View File

@@ -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;

View File

@@ -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

View File

@@ -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);
});
});

View File

@@ -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('*****');

View File

@@ -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) => {

View File

@@ -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';
}
return editor
&& typeof editor.getValue === 'function'
&& typeof editor.markText === 'function'
&& typeof editor.operation === 'function';
}

View File

@@ -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 };

View File

@@ -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) => {

View File

@@ -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', () => {

View File

@@ -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
};

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -559,7 +559,7 @@ describe('prepare-request: prepareRequest', () => {
selected: true
}]
}
},
}
};
const result = await prepareRequest(item);

View File

@@ -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",

View File

@@ -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

View File

@@ -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';

File diff suppressed because it is too large Load Diff

View File

@@ -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('<invalid>xml</invalid>')).rejects.toThrow('Import WSDL collection failed');
});
});

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -90,5 +90,4 @@ describe('prepareGqlIntrospectionRequest', () => {
expect(result.headers['X-API-Key']).toBe('{{process.env.MISSING_VAR}}');
});
});
});

View File

@@ -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();

View File

@@ -22,7 +22,6 @@ const safeParseJSON = (jsonString, context = 'JSON string') => {
}
};
class WsClient {
messageQueues = {};
activeConnections = new Map();

View File

@@ -54,4 +54,4 @@ server.on('upgrade', wsRouter);
server.listen(port, function () {
console.log(`Testbench started on port: ${port}`);
});
});

View File

@@ -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;
}
}

View File

@@ -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();
});

View File

@@ -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": "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Body><GetUserRequest xmlns=\"http://example.com/testservice\"><userId>string</userId><includeDetails>true</includeDetails></GetUserRequest></soap:Body></soap:Envelope>",
"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": "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Body><CreateUserRequest xmlns=\"http://example.com/testservice\"><name>string</name><email>string</email><password>string</password></CreateUserRequest></soap:Body></soap:Envelope>",
"formUrlEncoded": [],
"multipartForm": []
},
"script": {
"res": null
}
}
}
]
}
]
}

View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions
name="TestWSDLServiceXML"
targetNamespace="http://example.com/testservice"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:tns="http://example.com/testservice"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<wsdl:documentation>Test WSDL for Bruno import testing</wsdl:documentation>
<wsdl:types>
<xsd:schema targetNamespace="http://example.com/testservice" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="GetUserRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="userId" type="xsd:string"/>
<xsd:element name="includeDetails" type="xsd:boolean" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetUserResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="user" type="tns:User"/>
<xsd:element name="status" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="User">
<xsd:sequence>
<xsd:element name="id" type="xsd:string"/>
<xsd:element name="name" type="xsd:string"/>
<xsd:element name="email" type="xsd:string"/>
<xsd:element name="active" type="xsd:boolean"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="CreateUserRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="name" type="xsd:string"/>
<xsd:element name="email" type="xsd:string"/>
<xsd:element name="password" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="CreateUserResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="userId" type="xsd:string"/>
<xsd:element name="status" type="xsd:string"/>
<xsd:element name="message" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
</wsdl:types>
<wsdl:message name="GetUserRequestMessage">
<wsdl:part name="parameters" element="tns:GetUserRequest"/>
</wsdl:message>
<wsdl:message name="GetUserResponseMessage">
<wsdl:part name="parameters" element="tns:GetUserResponse"/>
</wsdl:message>
<wsdl:message name="CreateUserRequestMessage">
<wsdl:part name="parameters" element="tns:CreateUserRequest"/>
</wsdl:message>
<wsdl:message name="CreateUserResponseMessage">
<wsdl:part name="parameters" element="tns:CreateUserResponse"/>
</wsdl:message>
<wsdl:portType name="UserServicePortType">
<wsdl:documentation>User management service port type</wsdl:documentation>
<wsdl:operation name="GetUser">
<wsdl:documentation>Retrieve user information by ID</wsdl:documentation>
<wsdl:input message="tns:GetUserRequestMessage"/>
<wsdl:output message="tns:GetUserResponseMessage"/>
</wsdl:operation>
<wsdl:operation name="CreateUser">
<wsdl:documentation>Create a new user</wsdl:documentation>
<wsdl:input message="tns:CreateUserRequestMessage"/>
<wsdl:output message="tns:CreateUserResponseMessage"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="UserServiceBinding" type="tns:UserServicePortType">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="GetUser">
<soap:operation soapAction="http://example.com/testservice/GetUser"/>
<wsdl:input>
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output>
<soap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
<wsdl:operation name="CreateUser">
<soap:operation soapAction="http://example.com/testservice/CreateUser"/>
<wsdl:input>
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output>
<soap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="UserService">
<wsdl:documentation>User management web service</wsdl:documentation>
<wsdl:port name="UserServicePort" binding="tns:UserServiceBinding">
<soap:address location="http://example.com/soap/userservice"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>

View File

@@ -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();
});
});
});

View File

@@ -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');