diff --git a/packages/bruno-app/src/utils/exporters/postman-collection.js b/packages/bruno-app/src/utils/exporters/postman-collection.js index 7260942f4d..9dabc0f640 100644 --- a/packages/bruno-app/src/utils/exporters/postman-collection.js +++ b/packages/bruno-app/src/utils/exporters/postman-collection.js @@ -1,6 +1,105 @@ import map from 'lodash/map'; import * as FileSaver from 'file-saver'; -import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from 'utils/collections/export'; +import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from '../collections/export'; + +/** + * Transforms a given URL string into an object representing the protocol, host, path, query, and variables. + * + * @param {string} url - The raw URL to be transformed. + * @param {Object} params - The params object. + * @returns {Object|null} An object containing the URL's protocol, host, path, query, and variables, or {} if an error occurs. + */ +export const transformUrl = (url, params) => { + if (typeof url !== 'string' || !url.trim()) { + throw new Error("Invalid URL input"); + } + + const urlRegexPatterns = { + protocolAndRestSeparator: /:\/\//, + hostAndPathSeparator: /\/(.+)/, + domainSegmentSeparator: /\./, + pathSegmentSeparator: /\//, + queryStringSeparator: /\?/ + }; + + const postmanUrl = { raw: url }; + + /** + * Splits a URL into its protocol, host and path. + * + * @param {string} url - The URL to be split. + * @returns {Object} An object containing the protocol and the raw host/path string. + */ + const splitUrl = (url) => { + const urlParts = url.split(urlRegexPatterns.protocolAndRestSeparator); + if (urlParts.length === 1) { + return { protocol: '', rawHostAndPath: urlParts[0] }; + } else if (urlParts.length === 2) { + const [hostAndPath, _] = urlParts[1].split(urlRegexPatterns.queryStringSeparator); + return { protocol: urlParts[0], rawHostAndPath: hostAndPath }; + } else { + throw new Error(`Invalid URL format: ${url}`); + } + }; + + /** + * Splits the host and path from a raw host/path string. + * + * @param {string} rawHostAndPath - The raw host and path string to be split. + * @returns {Object} An object containing the host and path. + */ + const splitHostAndPath = (rawHostAndPath) => { + const [host, path = ''] = rawHostAndPath.split(urlRegexPatterns.hostAndPathSeparator); + return { host, path }; + }; + + try { + const { protocol, rawHostAndPath } = splitUrl(url); + postmanUrl.protocol = protocol; + + const { host, path } = splitHostAndPath(rawHostAndPath); + postmanUrl.host = host ? host.split(urlRegexPatterns.domainSegmentSeparator) : []; + postmanUrl.path = path ? path.split(urlRegexPatterns.pathSegmentSeparator) : []; + } catch (error) { + console.error(error.message); + return {}; + } + + // Construct query params. + postmanUrl.query = params + .filter((param) => param.type === 'query') + .map(({ name, value, description }) => ({ key: name, value, description })); + + // Construct path params. + postmanUrl.variable = params + .filter((param) => param.type === 'path') + .map(({ name, value, description }) => ({ key: name, value, description })); + + return postmanUrl; +}; + +/** + * Collapses multiple consecutive slashes (`//`) into a single slash, while skipping the protocol (e.g., `http://` or `https://`). + * + * @param {String} url - A URL string + * @returns {String} The sanitized URL + * + */ +const collapseDuplicateSlashes = (url) => { + return url.replace(/(? { + let sanitizedUrl = collapseDuplicateSlashes(url.replace(/\\/g, '//')); + return sanitizedUrl; +}; export const exportCollection = (collection) => { delete collection.uid; @@ -177,49 +276,17 @@ export const exportCollection = (collection) => { } }; - const generateHost = (url) => { - try { - const { hostname } = new URL(url); - return hostname.split('.'); - } catch (error) { - console.error(`Invalid URL: ${url}`, error); - return []; - } - }; - - const generatePathParams = (params) => { - return params.filter((param) => param.type === 'path').map((param) => `:${param.name}`); - }; - - const generateQueryParams = (params) => { - return params - .filter((param) => param.type === 'query') - .map(({ name, value, description }) => ({ key: name, value, description })); - }; - - const generateVariables = (params) => { - return params - .filter((param) => param.type === 'path') - .map(({ name, value, description }) => ({ key: name, value, description })); - }; - const generateRequestSection = (itemRequest) => { const requestObject = { method: itemRequest.method, header: generateHeaders(itemRequest.headers), auth: generateAuth(itemRequest.auth), description: itemRequest.docs, - url: { - raw: itemRequest.url, - host: generateHost(itemRequest.url), - path: generatePathParams(itemRequest.params), - query: generateQueryParams(itemRequest.params), - variable: generateVariables(itemRequest.params) - }, - auth: generateAuth(itemRequest.auth) + // We sanitize the URL to make sure it's in the right format before passing it to the transformUrl func. This means changing backslashes to forward slashes and reducing multiple slashes to a single one, except in the protocol part. + url: transformUrl(sanitizeUrl(itemRequest.url), itemRequest.params) }; - if (itemRequest.body.mode != 'none') { + if (itemRequest.body.mode !== 'none') { requestObject.body = generateBody(itemRequest.body); } return requestObject; diff --git a/packages/bruno-app/src/utils/exporters/postman-collection.spec.js b/packages/bruno-app/src/utils/exporters/postman-collection.spec.js new file mode 100644 index 0000000000..23c6690ccb --- /dev/null +++ b/packages/bruno-app/src/utils/exporters/postman-collection.spec.js @@ -0,0 +1,81 @@ +const { sanitizeUrl, transformUrl } = require('./postman-collection'); + +describe('transformUrl', () => { + it('should handle basic URL with path variables', () => { + const url = 'https://example.com/{{username}}/api/resource/:id'; + const params = [ + { name: 'id', value: '123', type: 'path' }, + ]; + + const result = transformUrl(url, params); + + expect(result).toEqual({ + raw: 'https://example.com/{{username}}/api/resource/:id', + protocol: 'https', + host: ['example', 'com'], + path: ['{{username}}', 'api', 'resource', ':id'], + query: [], + variable: [ + { key: 'id', value: '123' }, + ] + }); + }); + + it('should handle URL with query parameters', () => { + const url = 'https://example.com/api/resource?limit=10&offset=20'; + const params = [ + { name: 'limit', value: '10', type: 'query' }, + { name: 'offset', value: '20', type: 'query' } + ]; + + const result = transformUrl(url, params); + + expect(result).toEqual({ + raw: 'https://example.com/api/resource?limit=10&offset=20', + protocol: 'https', + host: ['example', 'com'], + path: ['api', 'resource'], + query: [ + { key: 'limit', value: '10' }, + { key: 'offset', value: '20' } + ], + variable: [] + }); + }); + + it('should handle URL without protocol', () => { + const url = 'example.com/api/resource'; + const params = []; + + const result = transformUrl(url, params); + + expect(result).toEqual({ + raw: 'example.com/api/resource', + protocol: '', + host: ['example', 'com'], + path: ['api', 'resource'], + query: [], + variable: [] + }); + }); +}); + +describe('sanitizeUrl', () => { + it('should replace backslashes with slashes', () => { + const input = 'http:\\\\example.com\\path\\to\\file'; + const expected = 'http://example.com/path/to/file'; + expect(sanitizeUrl(input)).toBe(expected); + }); + + it('should collapse multiple slashes into a single slash', () => { + const input = 'http://example.com//path///to////file'; + const expected = 'http://example.com/path/to/file'; + expect(sanitizeUrl(input)).toBe(expected); + }); + + it('should handle URLs with mixed slashes', () => { + const input = 'http:\\example.com//path\\to//file'; + const expected = 'http://example.com/path/to/file'; + expect(sanitizeUrl(input)).toBe(expected); + }); +})