From a538b27f249130e21e95ffb97f634fbd8dacbf42 Mon Sep 17 00:00:00 2001 From: Anton Date: Sat, 25 Oct 2025 11:50:18 +0200 Subject: [PATCH] Import WSDL to collection (#5015) * Import WSDL to bruno collection * feat(wsdl-import): remove unused code and minor refactor --------- Co-authored-by: Bijin Bruno --- eslint.config.js | 10 +- package-lock.json | 3 +- .../components/CollectionSettings/index.js | 4 +- .../src/components/Devtools/Console/index.js | 2 +- .../Devtools/Performance/StyledWrapper.js | 22 +- .../components/Devtools/Performance/index.js | 8 +- .../components/FolderSettings/Auth/index.js | 4 +- .../EnvironmentVariables/index.js | 2 +- .../src/components/GlobalSearchModal/index.js | 2 +- .../RequestTabs/RequestTab/index.js | 13 +- .../Sidebar/ImportCollection/index.js | 28 +- .../src/providers/App/useIpcEvents.js | 2 +- .../src/providers/ReduxStore/index.js | 2 +- .../ReduxStore/slices/performance.js | 10 +- .../bruno-app/src/utils/codegenerator/har.js | 8 +- .../src/utils/codemirror/brunoVarInfo.js | 6 +- .../src/utils/codemirror/brunoVarInfo.spec.js | 8 +- packages/bruno-app/src/utils/common/index.js | 2 +- .../src/utils/common/masked-editor.js | 22 +- .../src/utils/importers/wsdl-collection.js | 28 + packages/bruno-app/src/utils/url/index.js | 4 +- .../bruno-app/src/utils/url/index.spec.js | 1 - packages/bruno-cli/src/commands/import.js | 113 +- .../bruno-cli/src/runner/interpolate-vars.js | 2 +- .../bruno-cli/src/runner/prepare-request.js | 2 +- .../tests/runner/prepare-request.spec.js | 2 +- packages/bruno-converters/package.json | 3 +- packages/bruno-converters/readme.md | 75 ++ packages/bruno-converters/src/index.js | 1 + .../src/wsdl/wsdl-to-bruno.js | 1133 +++++++++++++++++ .../tests/wsdl/wsdl-to-bruno.spec.js | 16 + .../bruno-electron/src/app/system-monitor.js | 4 +- .../src/ipc/network/interpolate-vars.js | 6 +- .../tests/network/interpolate-vars.spec.js | 10 +- .../prepare-gql-introspection-request.spec.js | 3 +- .../sandbox/quickjs/shims/bruno-request.js | 2 +- packages/bruno-requests/src/ws/ws-client.js | 1 - packages/bruno-tests/src/index.js | 2 +- packages/bruno-tests/src/ws/index.js | 30 +- .../file-types/file-input-acceptance.spec.ts | 8 +- tests/import/wsdl/fixtures/wsdl-bruno.json | 102 ++ tests/import/wsdl/fixtures/wsdl.xml | 126 ++ tests/import/wsdl/import-wsdl.spec.ts | 127 ++ .../collection-run-report.spec.ts | 2 +- 44 files changed, 1826 insertions(+), 135 deletions(-) create mode 100644 packages/bruno-app/src/utils/importers/wsdl-collection.js create mode 100644 packages/bruno-converters/src/wsdl/wsdl-to-bruno.js create mode 100644 packages/bruno-converters/tests/wsdl/wsdl-to-bruno.spec.js create mode 100644 tests/import/wsdl/fixtures/wsdl-bruno.json create mode 100644 tests/import/wsdl/fixtures/wsdl.xml create mode 100644 tests/import/wsdl/import-wsdl.spec.ts 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)}`; + } else { + xml += `<${element.name}${attributes}>`; + xml += this.generateComplexContent(element); + xml += ``; + } + + 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');