diff --git a/packages/bruno-app/src/components/Sidebar/BulkImportCollectionLocation/index.js b/packages/bruno-app/src/components/Sidebar/BulkImportCollectionLocation/index.js
index 8e3b1c661..f4f051637 100644
--- a/packages/bruno-app/src/components/Sidebar/BulkImportCollectionLocation/index.js
+++ b/packages/bruno-app/src/components/Sidebar/BulkImportCollectionLocation/index.js
@@ -10,6 +10,7 @@ import { IconX, IconLoader2, IconCheck, IconCaretDown } from '@tabler/icons';
import InfoTip from 'components/InfoTip/index';
import Help from 'components/Help';
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
+import { addLog } from 'providers/ReduxStore/slices/logs';
import Dropdown from 'components/Dropdown';
import SelectionList from 'components/SelectionList';
import { postmanToBruno } from 'utils/importers/postman-collection';
@@ -19,6 +20,7 @@ import { processBrunoCollection } from 'utils/importers/bruno-collection';
import { wsdlToBruno } from '@usebruno/converters';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
+import { showImportIssuesToast } from 'components/Toast/ImportIssuesToast';
import get from 'lodash/get';
const STATUS = {
@@ -66,8 +68,10 @@ const getCollectionName = (format, rawData) => {
};
// Convert raw data to Bruno collection format
+// Returns { collection, issues } where issues tracks items that were skipped or degraded
const convertCollection = async (format, rawData, groupingType) => {
let collection;
+ let issues = [];
switch (format) {
case 'openapi':
@@ -76,9 +80,12 @@ const convertCollection = async (format, rawData, groupingType) => {
case 'wsdl':
collection = await wsdlToBruno(rawData);
break;
- case 'postman':
- collection = await postmanToBruno(rawData);
+ case 'postman': {
+ const result = await postmanToBruno(rawData);
+ collection = result.collection;
+ issues = result.issues || [];
break;
+ }
case 'insomnia':
collection = convertInsomniaToBruno(rawData);
break;
@@ -89,7 +96,7 @@ const convertCollection = async (format, rawData, groupingType) => {
throw new Error('Unknown collection format');
}
- return collection;
+ return { collection, issues };
};
export function normalizeName(name) {
@@ -150,6 +157,7 @@ export const BulkImportCollectionLocation = ({
const [collectionFormat, setCollectionFormat] = useState('bru');
const [renamedCollectionNames, setRenamedCollectionNames] = useState({});
const [renamedEnvironmentNames, setRenamedEnvironmentNames] = useState({});
+ const [importIssues, setImportIssues] = useState({});
// Extract data based on import type
const importType = importData?.type;
@@ -160,6 +168,21 @@ export const BulkImportCollectionLocation = ({
const importedCollectionFromBulk = isBulkImport ? importData.collection : [];
const importedEnvironmentFromBulk = isBulkImport ? (importData.environment || []) : [];
+ // Extract per-collection issues from bulk import data
+ useEffect(() => {
+ if (isBulkImport && importData.issues) {
+ const issuesMap = {};
+ importData.issues.forEach((entry, index) => {
+ if (entry.issues && entry.issues.length > 0 && importedCollectionFromBulk[index]) {
+ issuesMap[importedCollectionFromBulk[index].uid] = entry.issues;
+ }
+ });
+ setImportIssues(issuesMap);
+ } else {
+ setImportIssues({});
+ }
+ }, [isBulkImport, importData]);
+
// For multiple files import
const filesData = isMultipleImport ? importData.filesData : [];
const hasOpenApiSpec = filesData.some((f) => f.type === 'openapi');
@@ -281,19 +304,53 @@ export const BulkImportCollectionLocation = ({
if (isMultipleImport) {
// Convert selected files to collections at submit time
+ const collectedIssues = {};
for (const item of selectedItems) {
try {
- const collection = await convertCollection(item._fileData.type, item._fileData.data, groupingType);
+ const { collection, issues } = await convertCollection(item._fileData.type, item._fileData.data, groupingType);
if (collection) {
// Preserve the synthetic UID so status tracking, rename tracking,
// and UI rendering all use the same key
collection.uid = item.uid;
filteredCollections.push(collection);
+ if (issues && issues.length > 0) {
+ collectedIssues[item.uid] = issues;
+ }
}
} catch (err) {
console.warn(`Failed to convert file ${item._fileData.file.name}:`, err);
}
}
+ if (Object.keys(collectedIssues).length > 0) {
+ setImportIssues(collectedIssues);
+
+ const allIssues = [];
+ const timestamp = new Date().toISOString();
+ Object.entries(collectedIssues).forEach(([uid, issues]) => {
+ const item = selectedItems.find((s) => s.uid === uid);
+ const name = item?.name || uid;
+ const skipped = issues.filter((i) => i.severity === 'error').length;
+ const warnings = issues.filter((i) => i.severity === 'warning').length;
+ const parts = [];
+ if (skipped > 0) parts.push(`skipped ${skipped} item(s)`);
+ if (warnings > 0) parts.push(`${warnings} warning(s)`);
+
+ // Per-collection summary header
+ dispatch(addLog({ type: 'warn', args: [`Import: ${name} — ${parts.join(', ')}`], timestamp }));
+
+ // Individual issues for this collection
+ issues.forEach((issue) => {
+ allIssues.push({ ...issue, path: `${name} > ${issue.path}` });
+ const logType = issue.severity === 'error' ? 'error' : 'warn';
+ const logArgs = [`[${issue.path}] ${issue.message}`];
+ if (issue.sourceItem) logArgs.push(issue.sourceItem);
+ dispatch(addLog({ type: logType, args: logArgs, timestamp }));
+ });
+ });
+
+ // Single toast for all collections
+ showImportIssuesToast(allIssues);
+ }
} else if (isBulkImport) {
// For bulk import, use selected collections directly
filteredCollections = selectedItems;
@@ -608,6 +665,29 @@ export const BulkImportCollectionLocation = ({
See error
)}
+ {status[collection.uid] === STATUS.SUCCESS && importIssues[collection.uid] && (
+
+
+ {importIssues[collection.uid].filter((i) => i.severity === 'error').length} item(s) skipped
+
+ {
+ const text = importIssues[collection.uid]
+ .map((i) => `[${i.severity.toUpperCase()}] ${i.path} — ${i.message}`)
+ .join('\n');
+ try {
+ await navigator.clipboard.writeText(text);
+ toast.success('Copied to clipboard', { duration: 2000 });
+ } catch (err) {
+ toast.error('Failed to copy to clipboard', { duration: 3000 });
+ }
+ }}
+ className="text-yellow-600 text-xs hover:underline"
+ >
+ Copy
+
+
+ )}
))}
diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js
index 0840880cd..1739a6961 100644
--- a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js
+++ b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js
@@ -13,11 +13,13 @@ import { processBrunoCollection } from 'utils/importers/bruno-collection';
import { processOpenCollection } from 'utils/importers/opencollection';
import { wsdlToBruno } from '@usebruno/converters';
import { toastError } from 'utils/common/error';
+import { addLog } from 'providers/ReduxStore/slices/logs';
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
import Modal from 'components/Modal';
import Help from 'components/Help';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
+import { showImportIssuesToast } from 'components/Toast/ImportIssuesToast';
import { DEFAULT_COLLECTION_FORMAT } from 'utils/common/constants';
// Extract collection name from raw data
@@ -53,9 +55,11 @@ const getCollectionName = (format, rawData) => {
};
// Convert raw data to Bruno collection format
+// Returns { collection, issues } where issues tracks items that were skipped or degraded
const convertCollection = async (format, rawData, groupingType, collectionFormat) => {
try {
let collection;
+ let issues = [];
switch (format) {
case 'openapi':
@@ -64,9 +68,12 @@ const convertCollection = async (format, rawData, groupingType, collectionFormat
case 'wsdl':
collection = await wsdlToBruno(rawData);
break;
- case 'postman':
- collection = await postmanToBruno(rawData);
+ case 'postman': {
+ const result = await postmanToBruno(rawData);
+ collection = result.collection;
+ issues = result.issues || [];
break;
+ }
case 'insomnia':
collection = convertInsomniaToBruno(rawData);
break;
@@ -84,7 +91,7 @@ const convertCollection = async (format, rawData, groupingType, collectionFormat
throw new Error('Unknown collection format');
}
- return collection;
+ return { collection, issues };
} catch (err) {
console.error('Conversion error:', err);
toastError(err, 'Failed to convert collection');
@@ -135,7 +142,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format, sour
.required('Location is required')
}),
onSubmit: async (values) => {
- const convertedCollection = await convertCollection(format, rawData, groupingType, collectionFormat);
+ const { collection: convertedCollection, issues } = await convertCollection(format, rawData, groupingType, collectionFormat);
const options = { format: collectionFormat };
if (showCheckForSpecUpdatesOption && enableCheckForSpecUpdates) {
@@ -164,6 +171,26 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format, sour
}
handleSubmit(convertedCollection, values.collectionLocation, options);
+
+ if (issues && issues.length > 0) {
+ // Show toast with copy/report actions
+ showImportIssuesToast(issues);
+
+ // Log each issue to Bruno's internal console
+ const skipped = issues.filter((i) => i.severity === 'error').length;
+ const warnings = issues.filter((i) => i.severity === 'warning').length;
+ const parts = [];
+ if (skipped > 0) parts.push(`skipped ${skipped} item(s)`);
+ if (warnings > 0) parts.push(`${warnings} warning(s)`);
+ const timestamp = new Date().toISOString();
+ dispatch(addLog({ type: 'warn', args: [`Import: ${collectionName} — ${parts.join(', ')}`], timestamp }));
+ issues.forEach((issue) => {
+ const logType = issue.severity === 'error' ? 'error' : 'warn';
+ const logArgs = [`[${issue.path}] ${issue.message}`];
+ if (issue.sourceItem) logArgs.push(issue.sourceItem);
+ dispatch(addLog({ type: logType, args: logArgs, timestamp }));
+ });
+ }
}
});
diff --git a/packages/bruno-app/src/components/Toast/ImportIssuesToast/StyledWrapper.js b/packages/bruno-app/src/components/Toast/ImportIssuesToast/StyledWrapper.js
new file mode 100644
index 000000000..8f4f7dc38
--- /dev/null
+++ b/packages/bruno-app/src/components/Toast/ImportIssuesToast/StyledWrapper.js
@@ -0,0 +1,143 @@
+import styled from 'styled-components';
+import { rgba } from 'polished';
+
+const StyledWrapper = styled.div`
+ position: relative;
+ display: flex;
+ background: ${(props) => props.theme.background.base};
+ color: ${(props) => props.theme.text};
+ border: 1px solid ${(props) => props.theme.border.border2};
+ border-radius: ${(props) => props.theme.border.radius.md};
+ overflow: hidden;
+ max-width: 420px;
+ min-width: 380px;
+ margin-bottom: 2rem;
+ margin-right: 0.75rem;
+ box-shadow: ${(props) => props.theme.shadow.lg};
+ transition: all 0.3s ease;
+
+ .toast-accent {
+ width: 4px;
+ flex-shrink: 0;
+ border-radius: ${(props) => props.theme.border.radius.md} 0 0 ${(props) => props.theme.border.radius.md};
+ background: ${(props) => props.theme.colors.text.danger};
+ }
+
+ .toast-body {
+ flex: 1;
+ padding: 12px 14px;
+ }
+
+ .toast-close {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: ${(props) => props.theme.text};
+ border-radius: ${(props) => props.theme.border.radius.sm};
+ opacity: 0.7;
+ transition: opacity 0.2s ease, background-color 0.2s ease;
+
+ &:hover {
+ opacity: 1;
+ background-color: ${(props) => rgba(props.theme.text, 0.1)};
+ }
+ }
+
+ .toast-title {
+ font-size: 13px;
+ font-weight: 500;
+ margin-bottom: 4px;
+ color: ${(props) => props.theme.text};
+ }
+
+ .toast-hint {
+ font-size: 12px;
+ color: ${(props) => props.theme.colors.text.subtext1};
+ margin-bottom: 8px;
+ }
+
+ .toast-checkbox {
+ display: flex;
+ align-items: flex-start;
+ gap: 6px;
+ cursor: pointer;
+ margin-bottom: 10px;
+
+ input[type='checkbox'] {
+ accent-color: ${(props) => props.theme.primary.solid};
+ cursor: pointer;
+ margin: 0;
+ margin-top: 2px;
+ flex-shrink: 0;
+ }
+
+ .toast-checkbox-text {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ .toast-checkbox-label {
+ font-size: 12px;
+ color: ${(props) => props.theme.text};
+ }
+
+ .toast-checkbox-desc {
+ font-size: 11px;
+ color: ${(props) => props.theme.colors.text.subtext2};
+ line-height: 1.4;
+ }
+ }
+
+ .toast-warning {
+ display: flex;
+ align-items: flex-start;
+ gap: 6px;
+ font-size: 11px;
+ color: ${(props) => props.theme.status.warning.text};
+ background: ${(props) => props.theme.status.warning.background};
+ border: 1px solid ${(props) => props.theme.status.warning.border};
+ border-radius: ${(props) => props.theme.border.radius.sm};
+ padding: 6px 8px;
+ margin-bottom: 8px;
+ line-height: 1.4;
+
+ .toast-warning-icon {
+ flex-shrink: 0;
+ margin-top: 1px;
+ }
+ }
+
+ .toast-actions {
+ display: flex;
+ gap: 8px;
+ justify-content: flex-end;
+ }
+
+ .toast-btn {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 12px;
+ padding: 4px 10px;
+ cursor: pointer;
+ border: 1px solid ${(props) => props.theme.border.border1};
+ border-radius: ${(props) => props.theme.border.radius.sm};
+ background: ${(props) => props.theme.background.surface1};
+ color: ${(props) => props.theme.text};
+
+ &:hover {
+ background: ${(props) => props.theme.background.surface2};
+ }
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/Toast/ImportIssuesToast/index.js b/packages/bruno-app/src/components/Toast/ImportIssuesToast/index.js
new file mode 100644
index 000000000..b84e28159
--- /dev/null
+++ b/packages/bruno-app/src/components/Toast/ImportIssuesToast/index.js
@@ -0,0 +1,176 @@
+import React, { useState, useMemo } from 'react';
+import toast from 'react-hot-toast';
+import { IconAlertCircle, IconBrandGithub, IconCopy, IconX } from '@tabler/icons';
+import StyledWrapper from './StyledWrapper';
+
+const GITHUB_ISSUES_URL = 'https://github.com/usebruno/bruno/issues/new';
+const MAX_URL_LENGTH = 8000;
+
+const ImportIssuesToastContent = ({ t, issues, summary }) => {
+ const [includeItems, setIncludeItems] = useState(false);
+ const hasSourceItems = issues.some((i) => i.sourceItem);
+
+ const issuesSummary = issues.map((i) => `[${i.severity.toUpperCase()}] ${i.path} — ${i.message}`).join('\n');
+
+ const buildIssueBody = () => {
+ const sections = [
+ '### Description',
+ 'Postman collection import completed with issues. Some items could not be converted.',
+ '',
+ '### Import Issues',
+ '```',
+ issuesSummary,
+ '```'
+ ];
+
+ if (includeItems) {
+ const itemsWithSource = issues.filter((i) => i.sourceItem);
+ if (itemsWithSource.length > 0) {
+ const itemsJson = itemsWithSource
+ .map((i) => `// ${i.path}\n${JSON.stringify(i.sourceItem, null, 2)}`)
+ .join('\n\n');
+ sections.push(
+ '',
+ '### Failed Items',
+ '> **Please redact any sensitive information (API keys, tokens, passwords, internal URLs) before submitting.**',
+ '```json',
+ itemsJson,
+ '```'
+ );
+ }
+ }
+
+ sections.push(
+ '',
+ '### Steps to Reproduce',
+ '1. Import the attached Postman collection (redact sensitive data before attaching)',
+ '2. ',
+ '',
+ '### Additional Context',
+ ''
+ );
+
+ return sections.join('\n');
+ };
+
+ const isUrlTooLong = useMemo(() => {
+ const title = `Postman import: ${summary}`;
+ const body = buildIssueBody();
+ const params = new URLSearchParams({ title, body, labels: 'bug' });
+ return `${GITHUB_ISSUES_URL}?${params.toString()}`.length > MAX_URL_LENGTH;
+ }, [issues, summary, includeItems]);
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(issuesSummary);
+ toast.success('Copied to clipboard', { duration: 2000 });
+ } catch (err) {
+ toast.error('Failed to copy to clipboard', { duration: 3000 });
+ }
+ };
+
+ const handleReport = async () => {
+ const title = `Postman import: ${summary}`;
+ const body = buildIssueBody();
+
+ if (!isUrlTooLong) {
+ const params = new URLSearchParams({ title, body, labels: 'bug' });
+ window.open(`${GITHUB_ISSUES_URL}?${params.toString()}`, '_blank');
+ return;
+ }
+
+ try {
+ await navigator.clipboard.writeText(body);
+ toast.success('Issue details copied — paste them into the GitHub issue body', { duration: 5000 });
+ } catch (err) {
+ toast.error('Failed to copy to clipboard', { duration: 3000 });
+ }
+ const params = new URLSearchParams({ title, labels: 'bug' });
+ window.open(`${GITHUB_ISSUES_URL}?${params.toString()}`, '_blank');
+ };
+
+ return (
+
+
+
+
toast.dismiss(t.id)}
+ >
+
+
+
Imported with issues: {summary}
+
Open DevTools console to see which items failed and why.
+ {hasSourceItems && (
+
+ setIncludeItems(e.target.checked)}
+ data-testid="import-issues-include-items-checkbox"
+ />
+
+ Include failed request data
+ Attaches the raw Postman request items that failed. May contain API keys, tokens, or internal URLs.
+
+
+ )}
+ {isUrlTooLong && (
+
+
+ Issue details are too long to embed in the URL. Clicking "Report on GitHub" will copy them to your clipboard — paste it once the GitHub issue page opens.
+
+ )}
+
+
+
+ Report on GitHub
+
+
+
+ Copy Issues
+
+
+
+
+ );
+};
+
+/**
+ * Show an import issues toast in the bottom-right corner.
+ * Aggregates all issues into a single toast — does not stack.
+ */
+let activeImportToastId = null;
+
+export const showImportIssuesToast = (issues) => {
+ if (activeImportToastId) {
+ toast.dismiss(activeImportToastId);
+ }
+
+ const errors = issues.filter((i) => i.severity === 'error');
+ const warnings = issues.filter((i) => i.severity === 'warning');
+ const parts = [];
+ if (errors.length > 0) parts.push(`${errors.length} item(s) skipped`);
+ if (warnings.length > 0) parts.push(`${warnings.length} warning(s)`);
+ const summary = parts.join(', ');
+
+ activeImportToastId = toast.custom(
+ (t) => (
+
+ ),
+ { duration: Infinity, position: 'bottom-right' }
+ );
+
+ return activeImportToastId;
+};
+
+export default ImportIssuesToastContent;
diff --git a/packages/bruno-converters/src/postman/postman-to-bruno.js b/packages/bruno-converters/src/postman/postman-to-bruno.js
index 5c6b3f7ed..5f27cb598 100644
--- a/packages/bruno-converters/src/postman/postman-to-bruno.js
+++ b/packages/bruno-converters/src/postman/postman-to-bruno.js
@@ -363,12 +363,20 @@ export const processAuth = (auth, requestObject, isCollection = false) => {
}
};
-const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false } = {}, scriptMap) => {
+const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false } = {}, scriptMap, issues = [], parentPath = '') => {
brunoParent.items = brunoParent.items || [];
const folderMap = {};
const requestMap = {};
item.forEach((i, index) => {
+ if (typeof i !== 'object' || i === null) {
+ issues.push({ path: parentPath ? `${parentPath} / Item ${index + 1}` : `Item ${index + 1}`, severity: 'error', message: 'Malformed collection item (not an object)' });
+ return;
+ }
+
+ const itemName = i.name || `Item ${index + 1}`;
+ const itemPath = parentPath ? `${parentPath} / ${itemName}` : itemName;
+
if (isItemAFolder(i)) {
const baseFolderName = i.name || 'Untitled Folder';
let folderName = baseFolderName;
@@ -415,7 +423,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false }
processAuth(i.auth, brunoFolderItem.root.request);
if (i.item && i.item.length) {
- importPostmanV2CollectionItem(brunoFolderItem, i.item, { useWorkers }, scriptMap);
+ importPostmanV2CollectionItem(brunoFolderItem, i.item, { useWorkers }, scriptMap, issues, itemPath);
}
if (i.event) {
@@ -431,377 +439,381 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false }
folderMap[folderName] = brunoFolderItem;
} else if (i.request) {
- const method = i?.request?.method?.toUpperCase();
- if (!method || typeof method !== 'string' || !method.trim()) {
- console.warn('Missing or invalid request.method', method);
+ const rawMethod = i?.request?.method;
+ if (!rawMethod || typeof rawMethod !== 'string' || !rawMethod.trim()) {
+ issues.push({ path: itemPath, severity: 'error', message: 'Missing or invalid request method', sourceItem: i });
return;
}
+ const method = rawMethod.toUpperCase();
- const baseRequestName = i.name || 'Untitled Request';
- let requestName = baseRequestName;
- let count = 1;
+ try {
+ const baseRequestName = i.name || 'Untitled Request';
+ let requestName = baseRequestName;
+ let count = 1;
- while (requestMap[requestName]) {
- requestName = `${baseRequestName}_${count}`;
- count++;
- }
-
- const url = constructUrl(i.request.url);
-
- const brunoRequestItem = {
- uid: uuid(),
- name: requestName,
- type: 'http-request',
- seq: index + 1,
- request: {
- url: url,
- method: method,
- auth: {
- mode: 'inherit',
- basic: null,
- bearer: null,
- awsv4: null,
- apikey: null,
- oauth1: null,
- oauth2: null,
- digest: null
- },
- headers: [],
- params: [],
- body: {
- mode: 'none',
- json: null,
- text: null,
- xml: null,
- formUrlEncoded: [],
- multipartForm: []
- },
- docs: transformDescription(i.request.description)
- }
- };
-
- const settings = {
- encodeUrl: i.protocolProfileBehavior?.disableUrlEncoding !== true
- };
-
- // Handle followRedirects setting
- if (i.protocolProfileBehavior?.followRedirects !== undefined) {
- settings.followRedirects = i.protocolProfileBehavior.followRedirects;
- }
-
- // Handle maxRedirects setting
- if (i.protocolProfileBehavior?.maxRedirects !== undefined) {
- settings.maxRedirects = i.protocolProfileBehavior.maxRedirects;
- }
-
- brunoRequestItem.settings = settings;
-
- brunoParent.items.push(brunoRequestItem);
-
- if (i.event) {
- if (useWorkers) {
- scriptMap.set(brunoRequestItem.uid, {
- events: i.event,
- request: brunoRequestItem.request
- });
- } else {
- i.event.forEach((event) => {
- if (event.listen === 'prerequest' && event.script && event.script.exec) {
- if (!brunoRequestItem.request?.script) {
- brunoRequestItem.request.script = {};
- }
- if (event.script.exec && event.script.exec.length > 0) {
- brunoRequestItem.request.script.req = postmanTranslation(event.script.exec);
- } else {
- brunoRequestItem.request.script.req = '';
- console.warn('Unexpected event.script.exec type', typeof event.script.exec);
- }
- }
- if (event.listen === 'test' && event.script && event.script.exec) {
- if (!brunoRequestItem.request?.script) {
- brunoRequestItem.request.script = {};
- }
- if (event.script.exec && event.script.exec.length > 0) {
- brunoRequestItem.request.script.res = postmanTranslation(event.script.exec);
- } else {
- brunoRequestItem.request.script.res = '';
- console.warn('Unexpected event.script.exec type', typeof event.script.exec);
- }
- }
- });
- }
- }
-
- const bodyMode = get(i, 'request.body.mode');
- if (bodyMode) {
- if (bodyMode === 'formdata') {
- brunoRequestItem.request.body.mode = 'multipartForm';
-
- each(i.request.body.formdata, (param) => {
- if (param.key == null && param.value == null) return;
- const isFile = param.type === 'file' || (param.type === 'default' && param.src);
- const value = isFile
- ? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : [])
- : (Array.isArray(param.value) ? param.value.join('') : ensureString(param.value));
-
- brunoRequestItem.request.body.multipartForm.push({
- uid: uuid(),
- type: isFile ? 'file' : 'text',
- name: ensureString(param.key),
- value,
- description: transformDescription(param.description),
- enabled: !param.disabled,
- ...(param.contentType && { contentType: param.contentType })
- });
- });
+ while (requestMap[requestName]) {
+ requestName = `${baseRequestName}_${count}`;
+ count++;
}
- if (bodyMode === 'urlencoded') {
- brunoRequestItem.request.body.mode = 'formUrlEncoded';
- each(i.request.body.urlencoded, (param) => {
- if (param.key == null && param.value == null) return;
- brunoRequestItem.request.body.formUrlEncoded.push({
- uid: uuid(),
- name: ensureString(param.key),
- value: ensureString(param.value),
- description: transformDescription(param.description),
- enabled: !param.disabled
- });
- });
- }
+ const url = constructUrl(i.request.url);
- if (bodyMode === 'raw') {
- let language = get(i, 'request.body.options.raw.language');
- if (!language) {
- language = searchLanguageByHeader(i.request.header);
- }
- if (language === 'json') {
- brunoRequestItem.request.body.mode = 'json';
- brunoRequestItem.request.body.json = i.request.body.raw;
- } else if (language === 'xml') {
- brunoRequestItem.request.body.mode = 'xml';
- brunoRequestItem.request.body.xml = i.request.body.raw;
- } else {
- brunoRequestItem.request.body.mode = 'text';
- brunoRequestItem.request.body.text = i.request.body.raw;
- }
- }
- }
-
- if (bodyMode === 'graphql') {
- brunoRequestItem.type = 'graphql-request';
- brunoRequestItem.request.body.mode = 'graphql';
- brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql);
- }
-
- each(normalizeHeaders(i.request.header), (header) => {
- if (header.key == null && header.value == null) return;
- brunoRequestItem.request.headers.push({
+ const brunoRequestItem = {
uid: uuid(),
- name: ensureString(header.key),
- value: ensureString(header.value),
- description: transformDescription(header.description),
- enabled: !header.disabled
- });
- });
-
- // Request-level auth
- processAuth(i.request.auth, brunoRequestItem.request);
-
- each(get(i, 'request.url.query'), (param) => {
- if (param.key == null && param.value == null) {
- return;
- }
- brunoRequestItem.request.params.push({
- uid: uuid(),
- name: ensureString(param.key),
- value: ensureString(param.value),
- description: transformDescription(param.description),
- type: 'query',
- enabled: !param.disabled
- });
- });
-
- each(get(i, 'request.url.variable', []), (param) => {
- if (!param.key) {
- // If no key, skip this iteration and discard the param
- return;
- }
-
- brunoRequestItem.request.params.push({
- uid: uuid(),
- name: ensureString(param.key),
- value: ensureString(param.value),
- description: transformDescription(param.description),
- type: 'path',
- enabled: true
- });
- });
-
- // Handle Postman examples (responses)
- if (i.response && Array.isArray(i.response)) {
- brunoRequestItem.examples = [];
-
- i.response.forEach((response, responseIndex) => {
- const sanitized = String(response.name ?? '').replace(/\r?\n/g, ' ').trim();
- const exampleName = sanitized || `Example ${responseIndex + 1}`;
-
- // Convert originalRequest to Bruno request format
- const originalRequest = response.originalRequest || {};
- const exampleUrl = constructUrl(originalRequest.url);
- const exampleMethod = originalRequest.method?.toUpperCase() || method;
-
- const example = {
- uid: uuid(),
- itemUid: brunoRequestItem.uid,
- name: exampleName,
- description: '',
- type: 'http-request',
- request: {
- url: exampleUrl,
- method: exampleMethod,
- headers: [],
- params: [],
- body: {
- mode: 'none',
- json: null,
- text: null,
- xml: null,
- formUrlEncoded: [],
- multipartForm: []
- }
+ name: requestName,
+ type: 'http-request',
+ seq: index + 1,
+ request: {
+ url: url,
+ method: method,
+ auth: {
+ mode: 'inherit',
+ basic: null,
+ bearer: null,
+ awsv4: null,
+ apikey: null,
+ oauth1: null,
+ oauth2: null,
+ digest: null
},
- response: {
- status: response.code || null,
- statusText: response.status || '',
- headers: [],
- body: {
- type: getBodyTypeFromContentTypeHeader(response.header),
- content: response.body || ''
- }
- }
- };
+ headers: [],
+ params: [],
+ body: {
+ mode: 'none',
+ json: null,
+ text: null,
+ xml: null,
+ formUrlEncoded: [],
+ multipartForm: []
+ },
+ docs: transformDescription(i.request.description)
+ }
+ };
- // Convert original request headers
- if (originalRequest.header) {
- normalizeHeaders(originalRequest.header).forEach((header) => {
- if (header.key == null && header.value == null) return;
- example.request.headers.push({
+ const settings = {
+ encodeUrl: i.protocolProfileBehavior?.disableUrlEncoding !== true
+ };
+
+ // Handle followRedirects setting
+ if (i.protocolProfileBehavior?.followRedirects !== undefined) {
+ settings.followRedirects = i.protocolProfileBehavior.followRedirects;
+ }
+
+ // Handle maxRedirects setting
+ if (i.protocolProfileBehavior?.maxRedirects !== undefined) {
+ settings.maxRedirects = i.protocolProfileBehavior.maxRedirects;
+ }
+
+ brunoRequestItem.settings = settings;
+
+ if (i.event) {
+ if (useWorkers) {
+ scriptMap.set(brunoRequestItem.uid, {
+ events: i.event,
+ request: brunoRequestItem.request
+ });
+ } else {
+ i.event.forEach((event) => {
+ if (event.listen === 'prerequest' && event.script && event.script.exec) {
+ if (!brunoRequestItem.request?.script) {
+ brunoRequestItem.request.script = {};
+ }
+ if (event.script.exec && event.script.exec.length > 0) {
+ brunoRequestItem.request.script.req = postmanTranslation(event.script.exec);
+ } else {
+ brunoRequestItem.request.script.req = '';
+ console.warn('Unexpected event.script.exec type', typeof event.script.exec);
+ }
+ }
+ if (event.listen === 'test' && event.script && event.script.exec) {
+ if (!brunoRequestItem.request?.script) {
+ brunoRequestItem.request.script = {};
+ }
+ if (event.script.exec && event.script.exec.length > 0) {
+ brunoRequestItem.request.script.res = postmanTranslation(event.script.exec);
+ } else {
+ brunoRequestItem.request.script.res = '';
+ console.warn('Unexpected event.script.exec type', typeof event.script.exec);
+ }
+ }
+ });
+ }
+ }
+
+ const bodyMode = get(i, 'request.body.mode');
+ if (bodyMode) {
+ if (bodyMode === 'formdata') {
+ brunoRequestItem.request.body.mode = 'multipartForm';
+
+ each(i.request.body.formdata, (param) => {
+ if (param.key == null && param.value == null) return;
+ const isFile = param.type === 'file' || (param.type === 'default' && param.src);
+ const value = isFile
+ ? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : [])
+ : (Array.isArray(param.value) ? param.value.join('') : ensureString(param.value));
+
+ brunoRequestItem.request.body.multipartForm.push({
uid: uuid(),
- name: ensureString(header.key),
- value: ensureString(header.value),
- description: transformDescription(header.description),
- enabled: !header.disabled
+ type: isFile ? 'file' : 'text',
+ name: ensureString(param.key),
+ value,
+ description: transformDescription(param.description),
+ enabled: !param.disabled,
+ ...(param.contentType && { contentType: param.contentType })
});
});
}
- // Convert original request query parameters
- if (originalRequest.url && originalRequest.url.query && Array.isArray(originalRequest.url.query)) {
- originalRequest.url.query.forEach((param) => {
- if (param.key == null && param.value == null) {
- return;
- }
- example.request.params.push({
+ if (bodyMode === 'urlencoded') {
+ brunoRequestItem.request.body.mode = 'formUrlEncoded';
+ each(i.request.body.urlencoded, (param) => {
+ if (param.key == null && param.value == null) return;
+ brunoRequestItem.request.body.formUrlEncoded.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
- type: 'query',
enabled: !param.disabled
});
});
}
- if (originalRequest.url && originalRequest.url.variable && Array.isArray(originalRequest.url.variable)) {
- originalRequest.url.variable.forEach((param) => {
- if (!param.key) return;
- example.request.params.push({
- uid: uuid(),
- name: ensureString(param.key),
- value: ensureString(param.value),
- description: transformDescription(param.description),
- type: 'path',
- enabled: true
- });
- });
- }
-
- // Convert original request body
- if (originalRequest.body) {
- const bodyMode = originalRequest.body.mode;
- if (bodyMode === 'formdata') {
- example.request.body.mode = 'multipartForm';
- if (originalRequest.body.formdata && Array.isArray(originalRequest.body.formdata)) {
- originalRequest.body.formdata.forEach((param) => {
- if (param.key == null && param.value == null) return;
- const isFile = param.type === 'file' || (param.type === 'default' && param.src);
- const value = isFile
- ? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : [])
- : (Array.isArray(param.value) ? param.value.join('') : ensureString(param.value));
-
- example.request.body.multipartForm.push({
- uid: uuid(),
- type: isFile ? 'file' : 'text',
- name: ensureString(param.key),
- value,
- description: transformDescription(param.description),
- enabled: !param.disabled,
- ...(param.contentType && { contentType: param.contentType })
- });
- });
- }
- } else if (bodyMode === 'urlencoded') {
- example.request.body.mode = 'formUrlEncoded';
- if (originalRequest.body.urlencoded && Array.isArray(originalRequest.body.urlencoded)) {
- originalRequest.body.urlencoded.forEach((param) => {
- if (param.key == null && param.value == null) return;
- example.request.body.formUrlEncoded.push({
- uid: uuid(),
- name: ensureString(param.key),
- value: ensureString(param.value),
- description: transformDescription(param.description),
- enabled: !param.disabled
- });
- });
- }
- } else if (bodyMode === 'raw') {
- let language = get(originalRequest, 'body.options.raw.language');
- if (!language) {
- language = searchLanguageByHeader(originalRequest.header || []);
- }
- if (language === 'json') {
- example.request.body.mode = 'json';
- example.request.body.json = originalRequest.body.raw;
- } else if (language === 'xml') {
- example.request.body.mode = 'xml';
- example.request.body.xml = originalRequest.body.raw;
- } else {
- example.request.body.mode = 'text';
- example.request.body.text = originalRequest.body.raw;
- }
+ if (bodyMode === 'raw') {
+ let language = get(i, 'request.body.options.raw.language');
+ if (!language) {
+ language = searchLanguageByHeader(i.request.header);
+ }
+ if (language === 'json') {
+ brunoRequestItem.request.body.mode = 'json';
+ brunoRequestItem.request.body.json = i.request.body.raw;
+ } else if (language === 'xml') {
+ brunoRequestItem.request.body.mode = 'xml';
+ brunoRequestItem.request.body.xml = i.request.body.raw;
+ } else {
+ brunoRequestItem.request.body.mode = 'text';
+ brunoRequestItem.request.body.text = i.request.body.raw;
}
}
+ }
- // Convert response headers
- if (response.header) {
- normalizeHeaders(response.header).forEach((header) => {
- if (header.key == null && header.value == null) return;
- example.response.headers.push({
- uid: uuid(),
- name: ensureString(header.key),
- value: ensureString(header.value),
- description: transformDescription(header.description),
- enabled: true
- });
- });
+ if (bodyMode === 'graphql') {
+ brunoRequestItem.type = 'graphql-request';
+ brunoRequestItem.request.body.mode = 'graphql';
+ brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql);
+ }
+
+ each(normalizeHeaders(i.request.header), (header) => {
+ if (header.key == null && header.value == null) return;
+ brunoRequestItem.request.headers.push({
+ uid: uuid(),
+ name: ensureString(header.key),
+ value: ensureString(header.value),
+ description: transformDescription(header.description),
+ enabled: !header.disabled
+ });
+ });
+
+ // Request-level auth
+ processAuth(i.request.auth, brunoRequestItem.request);
+
+ each(get(i, 'request.url.query'), (param) => {
+ if (param.key == null && param.value == null) {
+ return;
+ }
+ brunoRequestItem.request.params.push({
+ uid: uuid(),
+ name: ensureString(param.key),
+ value: ensureString(param.value),
+ description: transformDescription(param.description),
+ type: 'query',
+ enabled: !param.disabled
+ });
+ });
+
+ each(get(i, 'request.url.variable', []), (param) => {
+ if (!param.key) {
+ // If no key, skip this iteration and discard the param
+ return;
}
- brunoRequestItem.examples.push(example);
+ brunoRequestItem.request.params.push({
+ uid: uuid(),
+ name: ensureString(param.key),
+ value: ensureString(param.value),
+ description: transformDescription(param.description),
+ type: 'path',
+ enabled: true
+ });
});
- }
- requestMap[requestName] = brunoRequestItem;
+ // Handle Postman examples (responses)
+ if (i.response && Array.isArray(i.response)) {
+ brunoRequestItem.examples = [];
+
+ i.response.forEach((response, responseIndex) => {
+ const sanitized = String(response.name ?? '').replace(/\r?\n/g, ' ').trim();
+ const exampleName = sanitized || `Example ${responseIndex + 1}`;
+
+ // Convert originalRequest to Bruno request format
+ const originalRequest = response.originalRequest || {};
+ const exampleUrl = constructUrl(originalRequest.url);
+ const exampleMethod = originalRequest.method?.toUpperCase() || method;
+
+ const example = {
+ uid: uuid(),
+ itemUid: brunoRequestItem.uid,
+ name: exampleName,
+ description: '',
+ type: 'http-request',
+ request: {
+ url: exampleUrl,
+ method: exampleMethod,
+ headers: [],
+ params: [],
+ body: {
+ mode: 'none',
+ json: null,
+ text: null,
+ xml: null,
+ formUrlEncoded: [],
+ multipartForm: []
+ }
+ },
+ response: {
+ status: response.code || null,
+ statusText: response.status || '',
+ headers: [],
+ body: {
+ type: getBodyTypeFromContentTypeHeader(response.header),
+ content: response.body || ''
+ }
+ }
+ };
+
+ // Convert original request headers
+ if (originalRequest.header) {
+ normalizeHeaders(originalRequest.header).forEach((header) => {
+ if (header.key == null && header.value == null) return;
+ example.request.headers.push({
+ uid: uuid(),
+ name: ensureString(header.key),
+ value: ensureString(header.value),
+ description: transformDescription(header.description),
+ enabled: !header.disabled
+ });
+ });
+ }
+
+ // Convert original request query parameters
+ if (originalRequest.url && originalRequest.url.query && Array.isArray(originalRequest.url.query)) {
+ originalRequest.url.query.forEach((param) => {
+ if (param.key == null && param.value == null) {
+ return;
+ }
+ example.request.params.push({
+ uid: uuid(),
+ name: ensureString(param.key),
+ value: ensureString(param.value),
+ description: transformDescription(param.description),
+ type: 'query',
+ enabled: !param.disabled
+ });
+ });
+ }
+
+ if (originalRequest.url && originalRequest.url.variable && Array.isArray(originalRequest.url.variable)) {
+ originalRequest.url.variable.forEach((param) => {
+ if (!param.key) return;
+ example.request.params.push({
+ uid: uuid(),
+ name: ensureString(param.key),
+ value: ensureString(param.value),
+ description: transformDescription(param.description),
+ type: 'path',
+ enabled: true
+ });
+ });
+ }
+
+ // Convert original request body
+ if (originalRequest.body) {
+ const bodyMode = originalRequest.body.mode;
+ if (bodyMode === 'formdata') {
+ example.request.body.mode = 'multipartForm';
+ if (originalRequest.body.formdata && Array.isArray(originalRequest.body.formdata)) {
+ originalRequest.body.formdata.forEach((param) => {
+ if (param.key == null && param.value == null) return;
+ const isFile = param.type === 'file' || (param.type === 'default' && param.src);
+ const value = isFile
+ ? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : [])
+ : (Array.isArray(param.value) ? param.value.join('') : ensureString(param.value));
+
+ example.request.body.multipartForm.push({
+ uid: uuid(),
+ type: isFile ? 'file' : 'text',
+ name: ensureString(param.key),
+ value,
+ description: transformDescription(param.description),
+ enabled: !param.disabled,
+ ...(param.contentType && { contentType: param.contentType })
+ });
+ });
+ }
+ } else if (bodyMode === 'urlencoded') {
+ example.request.body.mode = 'formUrlEncoded';
+ if (originalRequest.body.urlencoded && Array.isArray(originalRequest.body.urlencoded)) {
+ originalRequest.body.urlencoded.forEach((param) => {
+ if (param.key == null && param.value == null) return;
+ example.request.body.formUrlEncoded.push({
+ uid: uuid(),
+ name: ensureString(param.key),
+ value: ensureString(param.value),
+ description: transformDescription(param.description),
+ enabled: !param.disabled
+ });
+ });
+ }
+ } else if (bodyMode === 'raw') {
+ let language = get(originalRequest, 'body.options.raw.language');
+ if (!language) {
+ language = searchLanguageByHeader(originalRequest.header || []);
+ }
+ if (language === 'json') {
+ example.request.body.mode = 'json';
+ example.request.body.json = originalRequest.body.raw;
+ } else if (language === 'xml') {
+ example.request.body.mode = 'xml';
+ example.request.body.xml = originalRequest.body.raw;
+ } else {
+ example.request.body.mode = 'text';
+ example.request.body.text = originalRequest.body.raw;
+ }
+ }
+ }
+
+ // Convert response headers
+ if (response.header) {
+ normalizeHeaders(response.header).forEach((header) => {
+ if (header.key == null && header.value == null) return;
+ example.response.headers.push({
+ uid: uuid(),
+ name: ensureString(header.key),
+ value: ensureString(header.value),
+ description: transformDescription(header.description),
+ enabled: true
+ });
+ });
+ }
+
+ brunoRequestItem.examples.push(example);
+ });
+ }
+
+ brunoParent.items.push(brunoRequestItem);
+ requestMap[requestName] = brunoRequestItem;
+ } catch (err) {
+ issues.push({ path: itemPath, severity: 'error', message: err.message, sourceItem: i });
+ }
}
});
};
@@ -876,8 +888,14 @@ const importPostmanV2Collection = async (collection, { useWorkers = false }) =>
importScriptsFromEvents(collection.event, brunoCollection.root.request);
}
+ const issues = [];
+
if (collection?.variable) {
- importCollectionLevelVariables(collection.variable, brunoCollection.root.request);
+ try {
+ importCollectionLevelVariables(collection.variable, brunoCollection.root.request);
+ } catch (err) {
+ issues.push({ path: 'Collection Variables', severity: 'warning', message: err.message });
+ }
}
// Collection level auth
@@ -886,7 +904,7 @@ const importPostmanV2Collection = async (collection, { useWorkers = false }) =>
// Create a single scriptMap for all items
const scriptMap = useWorkers ? new Map() : null;
- importPostmanV2CollectionItem(brunoCollection, collection.item, { useWorkers }, scriptMap);
+ importPostmanV2CollectionItem(brunoCollection, collection.item, { useWorkers }, scriptMap, issues);
// Process all scripts in a single call at the top level
if (useWorkers && scriptMap && scriptMap.size > 0) {
@@ -945,7 +963,7 @@ const importPostmanV2Collection = async (collection, { useWorkers = false }) =>
}
}
- return brunoCollection;
+ return { collection: brunoCollection, issues };
};
const parsePostmanCollection = async (collection, { useWorkers = false }) => {
@@ -979,13 +997,13 @@ const parsePostmanCollection = async (collection, { useWorkers = false }) => {
const postmanToBruno = async (postmanCollection, { useWorkers = false } = {}) => {
try {
- const parsedPostmanCollection = await parsePostmanCollection(postmanCollection, { useWorkers });
- const transformedCollection = transformItemsInCollection(parsedPostmanCollection);
+ const { collection: parsedCollection, issues } = await parsePostmanCollection(postmanCollection, { useWorkers });
+ const transformedCollection = transformItemsInCollection(parsedCollection);
const hydratedCollection = hydrateSeqInCollection(transformedCollection);
// Apply backward compatibility transformation for string status to number
const statusTransformedCollection = transformExampleStatusInCollection(hydratedCollection);
const validatedCollection = validateSchema(statusTransformedCollection);
- return validatedCollection;
+ return { collection: validatedCollection, issues };
} catch (err) {
console.log(err);
throw new Error(`Import collection failed: ${err.message}`);
diff --git a/packages/bruno-converters/tests/fixtures/postman-with-import-issues.json b/packages/bruno-converters/tests/fixtures/postman-with-import-issues.json
new file mode 100644
index 000000000..d221613a7
--- /dev/null
+++ b/packages/bruno-converters/tests/fixtures/postman-with-import-issues.json
@@ -0,0 +1,210 @@
+{
+ "info": {
+ "_postman_id": "test-import-issues-001",
+ "name": "Import Issues Test Collection",
+ "description": "A Postman collection designed to test partial import handling. Contains a mix of valid requests, requests with missing/invalid methods, and edge cases that should be gracefully handled.",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+ },
+ "item": [
+ {
+ "name": "Valid GET Request",
+ "request": {
+ "method": "GET",
+ "header": [
+ { "key": "Accept", "value": "application/json" }
+ ],
+ "url": {
+ "raw": "https://api.example.com/users",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["users"]
+ }
+ }
+ },
+ {
+ "name": "Missing Method (null)",
+ "request": {
+ "method": null,
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/should-be-skipped-1",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["should-be-skipped-1"]
+ }
+ }
+ },
+ {
+ "name": "Missing Method (absent)",
+ "request": {
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/should-be-skipped-2",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["should-be-skipped-2"]
+ }
+ }
+ },
+ {
+ "name": "Empty String Method",
+ "request": {
+ "method": "",
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/should-be-skipped-3",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["should-be-skipped-3"]
+ }
+ }
+ },
+ {
+ "name": "Whitespace-Only Method",
+ "request": {
+ "method": " ",
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/should-be-skipped-4",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["should-be-skipped-4"]
+ }
+ }
+ },
+ {
+ "name": "Valid POST Request",
+ "request": {
+ "method": "POST",
+ "header": [
+ { "key": "Content-Type", "value": "application/json" }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\"name\": \"test\", \"email\": \"test@example.com\"}",
+ "options": {
+ "raw": { "language": "json" }
+ }
+ },
+ "url": {
+ "raw": "https://api.example.com/users",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["users"]
+ }
+ }
+ },
+ {
+ "name": "API Folder",
+ "item": [
+ {
+ "name": "Valid Nested GET",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/nested/valid",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["nested", "valid"]
+ }
+ }
+ },
+ {
+ "name": "Nested Missing Method",
+ "request": {
+ "method": null,
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/nested/should-be-skipped",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["nested", "should-be-skipped"]
+ }
+ }
+ },
+ {
+ "name": "Deep Subfolder",
+ "item": [
+ {
+ "name": "Deep Valid Request",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/deep/valid",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["deep", "valid"]
+ }
+ }
+ },
+ {
+ "name": "Deep Bad Method",
+ "request": {
+ "method": null,
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/deep/should-be-skipped",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["deep", "should-be-skipped"]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Valid PUT Request",
+ "request": {
+ "method": "PUT",
+ "header": [
+ { "key": "Content-Type", "value": "application/json" },
+ { "key": "Authorization", "value": "Bearer {{token}}" }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\"name\": \"updated\"}",
+ "options": {
+ "raw": { "language": "json" }
+ }
+ },
+ "url": {
+ "raw": "https://api.example.com/users/{{userId}}",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["users", "{{userId}}"],
+ "variable": [
+ { "key": "userId", "value": "123" }
+ ]
+ }
+ }
+ },
+ {
+ "name": "No Request Object At All"
+ },
+ {
+ "name": "Valid PATCH Request",
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\"status\": \"active\"}"
+ },
+ "url": {
+ "raw": "https://api.example.com/users/1/status",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["users", "1", "status"]
+ }
+ }
+ }
+ ],
+ "variable": [
+ { "key": "baseUrl", "value": "https://api.example.com" },
+ { "key": "token", "value": "test-token-123" }
+ ]
+}
diff --git a/packages/bruno-converters/tests/postman-with-examples.spec.js b/packages/bruno-converters/tests/postman-with-examples.spec.js
index 15bbd52d8..670859042 100644
--- a/packages/bruno-converters/tests/postman-with-examples.spec.js
+++ b/packages/bruno-converters/tests/postman-with-examples.spec.js
@@ -101,7 +101,7 @@ describe('Postman to Bruno Converter with Examples', () => {
};
test('should convert Postman collection with examples to Bruno format', async () => {
- const brunoCollection = await postmanToBruno(postmanCollectionWithExamples);
+ const { collection: brunoCollection } = await postmanToBruno(postmanCollectionWithExamples);
expect(brunoCollection).toBeDefined();
expect(brunoCollection.name).toBe('collection with examples');
@@ -180,7 +180,7 @@ describe('Postman to Bruno Converter with Examples', () => {
]
};
- const brunoCollection = await postmanToBruno(postmanCollectionWithoutExamples);
+ const { collection: brunoCollection } = await postmanToBruno(postmanCollectionWithoutExamples);
expect(brunoCollection).toBeDefined();
expect(brunoCollection.name).toBe('collection without examples');
@@ -218,7 +218,7 @@ describe('Postman to Bruno Converter with Examples', () => {
]
};
- const brunoCollection = await postmanToBruno(postmanCollectionWithEmptyExamples);
+ const { collection: brunoCollection } = await postmanToBruno(postmanCollectionWithEmptyExamples);
expect(brunoCollection).toBeDefined();
expect(brunoCollection.name).toBe('collection with empty examples');
diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js
index 95b5ce8b6..45d0d4f38 100644
--- a/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js
+++ b/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js
@@ -29,7 +29,7 @@ describe('Collection Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
// console.log('result', JSON.stringify(result, null, 2));
expect(result.root.request.auth).toEqual({
@@ -86,7 +86,7 @@ describe('Collection Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
// console.log('result', JSON.stringify(result, null, 2));
expect(result.root.request.auth).toEqual({
@@ -141,7 +141,7 @@ describe('Collection Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
// console.log('result', JSON.stringify(result, null, 2));
expect(result.root.request.auth).toEqual({
@@ -200,7 +200,7 @@ describe('Collection Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.root.request.auth).toEqual({
mode: 'apikey',
@@ -265,7 +265,7 @@ describe('Collection Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.root.request.auth).toEqual({
mode: 'digest',
@@ -312,7 +312,7 @@ describe('Collection Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.root.request.auth).toEqual({
mode: 'basic',
@@ -360,7 +360,7 @@ describe('Collection Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.root.request.auth).toEqual({
mode: 'bearer',
diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js
index 4807c9d01..8fc257a83 100644
--- a/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js
+++ b/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js
@@ -49,7 +49,7 @@ describe('Folder Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].root.request.auth).toEqual({
mode: 'inherit',
@@ -113,7 +113,7 @@ describe('Folder Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].root.request.auth).toEqual({
mode: 'none',
@@ -174,7 +174,7 @@ describe('Folder Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].root.request.auth).toEqual({
mode: 'basic',
@@ -233,7 +233,7 @@ describe('Folder Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].root.request.auth).toEqual({
mode: 'bearer',
@@ -294,7 +294,7 @@ describe('Folder Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].root.request.auth).toEqual({
mode: 'apikey',
@@ -360,7 +360,7 @@ describe('Folder Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].root.request.auth).toEqual({
mode: 'digest',
@@ -410,7 +410,7 @@ describe('Folder Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].root.request.auth).toEqual({
mode: 'basic',
diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/partial-import.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/partial-import.spec.js
new file mode 100644
index 000000000..35bf9b662
--- /dev/null
+++ b/packages/bruno-converters/tests/postman/postman-to-bruno/partial-import.spec.js
@@ -0,0 +1,199 @@
+import { describe, it, expect } from '@jest/globals';
+import postmanToBruno from '../../../src/postman/postman-to-bruno';
+
+const makeCollection = (items, overrides = {}) => ({
+ info: {
+ _postman_id: 'test-id',
+ name: 'Test Collection',
+ schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
+ },
+ item: items,
+ ...overrides
+});
+
+const makeRequest = (name, method = 'GET', url = 'https://example.com') => ({
+ name,
+ request: {
+ method,
+ header: [],
+ url: { raw: url, protocol: 'https', host: ['example', 'com'] }
+ }
+});
+
+describe('partial-import', () => {
+ it('should import valid items and skip items with missing method', async () => {
+ const items = [
+ makeRequest('Valid Request 1'),
+ { name: 'Bad Request', request: { method: null, header: [], url: { raw: 'https://example.com' } } },
+ makeRequest('Valid Request 2')
+ ];
+
+ const { collection, issues } = await postmanToBruno(makeCollection(items));
+
+ expect(collection.items).toHaveLength(2);
+ expect(collection.items[0].name).toBe('Valid Request 1');
+ expect(collection.items[1].name).toBe('Valid Request 2');
+
+ expect(issues).toHaveLength(1);
+ expect(issues[0]).toMatchObject({
+ path: 'Bad Request',
+ severity: 'error',
+ message: 'Missing or invalid request method'
+ });
+ });
+
+ it('should import valid items and record errors for items that throw', async () => {
+ // Create a request with a value that will cause ensureString to throw (circular ref)
+ const circular = {};
+ circular.self = circular;
+
+ const items = [
+ makeRequest('Valid Request'),
+ {
+ name: 'Circular Request',
+ request: {
+ method: 'POST',
+ header: [{ key: circular, value: 'test' }],
+ url: { raw: 'https://example.com' }
+ }
+ }
+ ];
+
+ const { collection, issues } = await postmanToBruno(makeCollection(items));
+
+ expect(collection.items).toHaveLength(1);
+ expect(collection.items[0].name).toBe('Valid Request');
+
+ expect(issues).toHaveLength(1);
+ expect(issues[0].path).toBe('Circular Request');
+ expect(issues[0].severity).toBe('error');
+ });
+
+ it('should record issues for nested folder items with full path', async () => {
+ const items = [
+ {
+ name: 'My Folder',
+ item: [
+ {
+ name: 'Subfolder',
+ item: [
+ { name: 'Bad Nested', request: { method: null, header: [], url: { raw: 'https://example.com' } } },
+ makeRequest('Good Nested')
+ ]
+ }
+ ]
+ }
+ ];
+
+ const { collection, issues } = await postmanToBruno(makeCollection(items));
+
+ expect(collection.items).toHaveLength(1);
+ expect(collection.items[0].type).toBe('folder');
+ expect(collection.items[0].items[0].type).toBe('folder');
+ expect(collection.items[0].items[0].items).toHaveLength(1);
+ expect(collection.items[0].items[0].items[0].name).toBe('Good Nested');
+
+ expect(issues).toHaveLength(1);
+ expect(issues[0].path).toBe('My Folder / Subfolder / Bad Nested');
+ expect(issues[0].severity).toBe('error');
+ });
+
+ it('should return empty issues array for valid collections', async () => {
+ const items = [
+ makeRequest('Request 1'),
+ makeRequest('Request 2')
+ ];
+
+ const { collection, issues } = await postmanToBruno(makeCollection(items));
+
+ expect(collection.items).toHaveLength(2);
+ expect(issues).toEqual([]);
+ });
+
+ it('should handle empty collections with no issues', async () => {
+ const { collection, issues } = await postmanToBruno(makeCollection([]));
+
+ expect(collection.items).toEqual([]);
+ expect(issues).toEqual([]);
+ });
+
+ it('should handle all items being malformed without throwing', async () => {
+ const items = [
+ { name: 'Bad 1', request: { method: null, header: [], url: { raw: 'https://example.com' } } },
+ { name: 'Bad 2', request: { method: undefined, header: [], url: { raw: 'https://example.com' } } }
+ ];
+
+ const { collection, issues } = await postmanToBruno(makeCollection(items));
+
+ expect(collection.items).toEqual([]);
+ expect(issues).toHaveLength(2);
+ expect(issues.every((i) => i.severity === 'error')).toBe(true);
+ });
+
+ it('should record warning for malformed collection-level variables', async () => {
+ const collectionData = makeCollection([makeRequest('Valid')], {
+ variable: 'not-an-array'
+ });
+
+ const { collection, issues } = await postmanToBruno(collectionData);
+
+ expect(collection.items).toHaveLength(1);
+ const warnings = issues.filter((i) => i.severity === 'warning');
+ expect(warnings).toHaveLength(1);
+ expect(warnings[0].path).toBe('Collection Variables');
+ });
+
+ it('should handle mixed valid/invalid items with folders and bad variables', async () => {
+ const items = [
+ makeRequest('r1', 'POST'),
+ { name: 'r2-missing-method', request: { header: [], url: { raw: 'https://example.com' } } },
+ makeRequest('r3', 'POST'),
+ {
+ name: 'API Folder',
+ item: [
+ makeRequest('valid-in-folder', 'GET'),
+ { name: 'r4-null-method', request: { method: null, header: [], url: { raw: 'https://example.com' } } },
+ {
+ name: 'Nested Subfolder',
+ item: [
+ { name: 'r5-empty-method', request: { method: '', header: [], url: { raw: 'https://example.com' } } },
+ makeRequest('valid-nested', 'DELETE')
+ ]
+ }
+ ]
+ }
+ ];
+
+ const { collection, issues } = await postmanToBruno(makeCollection(items, { variable: 'not-an-array' }));
+
+ // 4 valid items: r1, r3, valid-in-folder, valid-nested
+ expect(collection.items).toHaveLength(3); // r1, r3, API Folder
+ expect(collection.items[0].name).toBe('r1');
+ expect(collection.items[1].name).toBe('r3');
+ expect(collection.items[2].name).toBe('API Folder');
+ expect(collection.items[2].items).toHaveLength(2); // valid-in-folder, Nested Subfolder
+ expect(collection.items[2].items[0].name).toBe('valid-in-folder');
+ expect(collection.items[2].items[1].name).toBe('Nested Subfolder');
+ expect(collection.items[2].items[1].items).toHaveLength(1); // valid-nested
+ expect(collection.items[2].items[1].items[0].name).toBe('valid-nested');
+
+ // 4 issues: 1 warning (variables) + 3 errors (missing/null/empty method)
+ expect(issues).toHaveLength(4);
+ expect(issues.filter((i) => i.severity === 'warning')).toHaveLength(1);
+ expect(issues.filter((i) => i.severity === 'error')).toHaveLength(3);
+ expect(issues.find((i) => i.path === 'r2-missing-method')).toBeTruthy();
+ expect(issues.find((i) => i.path === 'API Folder / r4-null-method')).toBeTruthy();
+ expect(issues.find((i) => i.path === 'API Folder / Nested Subfolder / r5-empty-method')).toBeTruthy();
+ });
+
+ it('should use fallback name for items without a name', async () => {
+ const items = [
+ { request: { method: null, header: [], url: { raw: 'https://example.com' } } }
+ ];
+
+ const { issues } = await postmanToBruno(makeCollection(items));
+
+ expect(issues).toHaveLength(1);
+ expect(issues[0].path).toBe('Item 1');
+ });
+});
diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js
index 1e02b4277..e6145969d 100644
--- a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js
+++ b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js
@@ -4,7 +4,7 @@ import { invalidVariableCharacterRegex } from '../../../src/constants';
describe('postman-collection', () => {
it('should correctly import a valid Postman collection file', async () => {
- const brunoCollection = await postmanToBruno(postmanCollection);
+ const { collection: brunoCollection } = await postmanToBruno(postmanCollection);
expect(brunoCollection).toMatchObject(expectedOutput);
});
@@ -55,7 +55,7 @@ describe('postman-collection', () => {
item: []
};
- const brunoCollection = await postmanToBruno(collectionWithFalsyVars);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithFalsyVars);
expect(brunoCollection.root.request.vars.req).toEqual([
{
@@ -125,7 +125,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithFalsyVars);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithFalsyVars);
expect(brunoCollection.items.map((item) => item.request.url)).toEqual([
'https://httpbin.org/api/v1/resource'
@@ -178,7 +178,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithFalsyVars);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithFalsyVars);
expect(brunoCollection.items.map((item) => item.request.url)).toEqual([
'https://httpbin.org/api/v1/resource/'
@@ -231,7 +231,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithFalsyVars);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithFalsyVars);
expect(brunoCollection.items.map((item) => item.request.url)).toEqual([
'https://httpbin.org/api//resource'
@@ -266,7 +266,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithNonStringVars);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithNonStringVars);
const vars = brunoCollection.root.request.vars.req;
expect(vars).toHaveLength(3);
@@ -286,7 +286,7 @@ describe('postman-collection', () => {
item: []
};
- const brunoCollection = await postmanToBruno(collectionWithEmptyVars);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithEmptyVars);
expect(brunoCollection.root.request.vars.req).toEqual([]);
});
@@ -348,7 +348,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithSettings);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithSettings);
// Test request with all settings
const requestWithAllSettings = brunoCollection.items[0];
@@ -402,7 +402,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithUndefinedAuthType);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithUndefinedAuthType);
// Collection level auth should default to 'none'
expect(brunoCollection.root.request.auth).toEqual({
@@ -459,7 +459,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithNullAuthType);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithNullAuthType);
// Collection level auth should default to 'none'
expect(brunoCollection.root.request.auth).toEqual({
@@ -505,7 +505,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithUnexpectedAuthType);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithUnexpectedAuthType);
// Collection level auth should default to 'none'
expect(brunoCollection.root.request.auth).toEqual({
@@ -562,7 +562,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithRequestUndefinedAuthType);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithRequestUndefinedAuthType);
// Collection level auth should default to 'none'
expect(brunoCollection.root.request.auth).toEqual({
@@ -624,7 +624,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithFolderUnexpectedAuthType);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithFolderUnexpectedAuthType);
// Folder auth should default to 'none'
expect(brunoCollection.items[0].root.request.auth).toEqual({
@@ -675,7 +675,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithNullHeaders);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithNullHeaders);
const headers = brunoCollection.items[0].request.headers;
expect(headers).toHaveLength(3);
@@ -715,7 +715,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithNullUrlencoded);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithNullUrlencoded);
const formUrlEncoded = brunoCollection.items[0].request.body.formUrlEncoded;
expect(formUrlEncoded).toHaveLength(3);
@@ -754,7 +754,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithNullFormdata);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithNullFormdata);
const multipartForm = brunoCollection.items[0].request.body.multipartForm;
expect(multipartForm).toHaveLength(2);
@@ -794,7 +794,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithNullQueryParams);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithNullQueryParams);
const params = brunoCollection.items[0].request.params;
// Fully-null entry should be skipped
@@ -871,7 +871,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithNumericValues);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithNumericValues);
const item = brunoCollection.items[0];
// Headers should have string values
@@ -950,7 +950,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithNumericExamples);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithNumericExamples);
const example = brunoCollection.items[0].examples[0];
// Example request headers
@@ -1011,7 +1011,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithNumericAuth);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithNumericAuth);
// Bearer token should be stringified
expect(brunoCollection.items[0].request.auth.mode).toBe('bearer');
@@ -1049,7 +1049,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithObjectAuth);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithObjectAuth);
expect(brunoCollection.items[0].request.auth.mode).toBe('basic');
expect(brunoCollection.items[0].request.auth.basic.username).toBe('12345');
@@ -1079,7 +1079,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithStringHeaders);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithStringHeaders);
const headers = brunoCollection.items[0].request.headers;
expect(headers).toHaveLength(3);
@@ -1110,7 +1110,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithConcatenatedHeaders);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithConcatenatedHeaders);
const headers = brunoCollection.items[0].request.headers;
expect(headers).toHaveLength(2);
@@ -1125,7 +1125,7 @@ describe('postman-collection', () => {
collection: { ...postmanCollection }
};
- const brunoCollection = await postmanToBruno(wrappedCollection);
+ const { collection: brunoCollection } = await postmanToBruno(wrappedCollection);
expect(brunoCollection).toMatchObject(expectedOutput);
});
@@ -1148,7 +1148,7 @@ describe('postman-collection', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithNoValueHeader);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithNoValueHeader);
const headers = brunoCollection.items[0].request.headers;
expect(headers).toHaveLength(1);
@@ -1247,7 +1247,7 @@ describe('postman-collection formdata import', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithFileFormdata);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithFileFormdata);
const multipartForm = brunoCollection.items[0].request.body.multipartForm;
expect(multipartForm).toHaveLength(1);
@@ -1287,7 +1287,7 @@ describe('postman-collection formdata import', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithDefaultTypeAndSrc);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithDefaultTypeAndSrc);
const multipartForm = brunoCollection.items[0].request.body.multipartForm;
expect(multipartForm).toHaveLength(1);
@@ -1327,7 +1327,7 @@ describe('postman-collection formdata import', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithDefaultTypeAndValueArray);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithDefaultTypeAndValueArray);
const multipartForm = brunoCollection.items[0].request.body.multipartForm;
expect(multipartForm).toHaveLength(1);
@@ -1368,7 +1368,7 @@ describe('postman-collection formdata import', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithContentType);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithContentType);
const multipartForm = brunoCollection.items[0].request.body.multipartForm;
expect(multipartForm).toHaveLength(1);
@@ -1412,7 +1412,7 @@ describe('postman-collection formdata import', () => {
]
};
- const brunoCollection = await postmanToBruno(collectionWithMixedFormdata);
+ const { collection: brunoCollection } = await postmanToBruno(collectionWithMixedFormdata);
const multipartForm = brunoCollection.items[0].request.body.multipartForm;
expect(multipartForm).toHaveLength(2);
diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js
index b6080f82d..dea2ee766 100644
--- a/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js
+++ b/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js
@@ -26,7 +26,7 @@ describe('Request Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].request.auth).toEqual({
mode: 'basic',
@@ -69,7 +69,7 @@ describe('Request Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].items[0].request.auth).toEqual({
mode: 'inherit',
@@ -110,7 +110,7 @@ describe('Request Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].items[0].request.auth).toEqual({
mode: 'inherit',
@@ -156,7 +156,7 @@ describe('Request Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
// Check folder first
expect(result.items[0].root.request.auth).toEqual({
@@ -199,7 +199,7 @@ describe('Request Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].items[0].request.auth).toEqual({
mode: 'none', // <<<< KEY CHECK
@@ -250,7 +250,7 @@ describe('Request Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
// Check Folder Level 1
expect(result.items[0].root.request.auth).toEqual({
@@ -311,7 +311,7 @@ describe('Request Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
// Check Folder Level 1
expect(result.items[0].root.request.auth).toEqual({
@@ -366,7 +366,7 @@ describe('Request Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].request.auth).toEqual({
mode: 'oauth1',
@@ -428,7 +428,7 @@ describe('Request Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].request.auth.mode).toBe('oauth1');
expect(result.items[0].request.auth.oauth1.placement).toBe('header');
@@ -471,7 +471,7 @@ describe('Request Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].request.auth.mode).toBe('oauth1');
expect(result.items[0].request.auth.oauth1.signatureMethod).toBe('RSA-SHA1');
@@ -508,7 +508,7 @@ describe('Request Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
// Collection root should have oauth1
expect(result.root.request.auth.mode).toBe('oauth1');
@@ -560,7 +560,7 @@ describe('Request Authentication', () => {
]
};
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
// Check Folder Level 1
expect(result.items[0].root.request.auth).toEqual({
@@ -606,7 +606,7 @@ describe('Request Authentication', () => {
{ key: 'in', value: 'query' }
]);
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].request.auth.mode).toBe('apikey');
expect(result.items[0].request.auth.apikey).toEqual({
@@ -623,7 +623,7 @@ describe('Request Authentication', () => {
{ key: 'in', value: 'header' }
]);
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].request.auth.apikey).toEqual({
key: 'X-API-Key',
@@ -638,7 +638,7 @@ describe('Request Authentication', () => {
{ key: 'value', value: 'secret-token' }
]);
- const result = await postmanToBruno(postmanCollection);
+ const { collection: result } = await postmanToBruno(postmanCollection);
expect(result.items[0].request.auth.apikey.placement).toBe('header');
});
diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/transform-description.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/transform-description.spec.js
index d47a92163..42e203db7 100644
--- a/packages/bruno-converters/tests/postman/postman-to-bruno/transform-description.spec.js
+++ b/packages/bruno-converters/tests/postman/postman-to-bruno/transform-description.spec.js
@@ -12,7 +12,7 @@ describe('transformDescription function', () => {
item: []
};
- const brunoCollection = await postmanToBruno(collection);
+ const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.root.docs).toBe('');
});
@@ -26,7 +26,7 @@ describe('transformDescription function', () => {
item: []
};
- const brunoCollection = await postmanToBruno(collection);
+ const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.root.docs).toBe('This is a string description');
});
@@ -43,7 +43,7 @@ describe('transformDescription function', () => {
item: []
};
- const brunoCollection = await postmanToBruno(collection);
+ const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.root.docs).toBe('This is the content from the new Postman format');
});
@@ -59,7 +59,7 @@ describe('transformDescription function', () => {
item: []
};
- const brunoCollection = await postmanToBruno(collection);
+ const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.root.docs).toBe('');
});
@@ -84,7 +84,7 @@ describe('transformDescription function', () => {
]
};
- const brunoCollection = await postmanToBruno(collection);
+ const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.items[0].request.docs).toBe('This is a request description in new format');
});
@@ -106,7 +106,7 @@ describe('transformDescription function', () => {
]
};
- const brunoCollection = await postmanToBruno(collection);
+ const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.items[0].root.docs).toBe('This is a folder description in new format');
});
@@ -137,7 +137,7 @@ describe('transformDescription function', () => {
]
};
- const brunoCollection = await postmanToBruno(collection);
+ const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.items[0].request.headers[0].description).toBe('Authorization header description');
});
@@ -172,7 +172,7 @@ describe('transformDescription function', () => {
]
};
- const brunoCollection = await postmanToBruno(collection);
+ const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.items[0].request.params[0].description).toBe('Query parameter description');
});
@@ -207,7 +207,7 @@ describe('transformDescription function', () => {
]
};
- const brunoCollection = await postmanToBruno(collection);
+ const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.items[0].request.params[0].description).toBe('User ID path variable');
});
@@ -241,7 +241,7 @@ describe('transformDescription function', () => {
]
};
- const brunoCollection = await postmanToBruno(collection);
+ const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.items[0].request.body.multipartForm[0].description).toBe('Form field description');
});
@@ -275,7 +275,7 @@ describe('transformDescription function', () => {
]
};
- const brunoCollection = await postmanToBruno(collection);
+ const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.items[0].request.body.formUrlEncoded[0].description).toBe('URL encoded field description');
});
@@ -317,7 +317,7 @@ describe('transformDescription function', () => {
]
};
- const brunoCollection = await postmanToBruno(collection);
+ const { collection: brunoCollection } = await postmanToBruno(collection);
// Collection description (string)
expect(brunoCollection.root.docs).toBe('Collection with string description');
@@ -357,7 +357,7 @@ describe('transformDescription function', () => {
]
};
- const brunoCollection = await postmanToBruno(collection);
+ const { collection: brunoCollection } = await postmanToBruno(collection);
expect(brunoCollection.root.docs).toBe('');
expect(brunoCollection.items[0].request.docs).toBe('Description with special chars: !@#$%^&*()');
});
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index e81662fb4..15006e6d5 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -2127,9 +2127,10 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
ipcMain.handle('renderer:convert-postman-to-bruno', async (event, postmanCollection) => {
try {
// Convert Postman collection to Bruno format
- const brunoCollection = await postmanToBruno(postmanCollection, { useWorkers: true });
+ // Returns { collection, issues } where issues tracks items that were skipped or degraded
+ const result = await postmanToBruno(postmanCollection, { useWorkers: true });
- return brunoCollection;
+ return result;
} catch (error) {
console.error('Error converting Postman to Bruno:', error);
return Promise.reject(error);
diff --git a/tests/import/postman/fixtures/postman-with-import-issues.json b/tests/import/postman/fixtures/postman-with-import-issues.json
new file mode 100644
index 000000000..d221613a7
--- /dev/null
+++ b/tests/import/postman/fixtures/postman-with-import-issues.json
@@ -0,0 +1,210 @@
+{
+ "info": {
+ "_postman_id": "test-import-issues-001",
+ "name": "Import Issues Test Collection",
+ "description": "A Postman collection designed to test partial import handling. Contains a mix of valid requests, requests with missing/invalid methods, and edge cases that should be gracefully handled.",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+ },
+ "item": [
+ {
+ "name": "Valid GET Request",
+ "request": {
+ "method": "GET",
+ "header": [
+ { "key": "Accept", "value": "application/json" }
+ ],
+ "url": {
+ "raw": "https://api.example.com/users",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["users"]
+ }
+ }
+ },
+ {
+ "name": "Missing Method (null)",
+ "request": {
+ "method": null,
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/should-be-skipped-1",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["should-be-skipped-1"]
+ }
+ }
+ },
+ {
+ "name": "Missing Method (absent)",
+ "request": {
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/should-be-skipped-2",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["should-be-skipped-2"]
+ }
+ }
+ },
+ {
+ "name": "Empty String Method",
+ "request": {
+ "method": "",
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/should-be-skipped-3",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["should-be-skipped-3"]
+ }
+ }
+ },
+ {
+ "name": "Whitespace-Only Method",
+ "request": {
+ "method": " ",
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/should-be-skipped-4",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["should-be-skipped-4"]
+ }
+ }
+ },
+ {
+ "name": "Valid POST Request",
+ "request": {
+ "method": "POST",
+ "header": [
+ { "key": "Content-Type", "value": "application/json" }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\"name\": \"test\", \"email\": \"test@example.com\"}",
+ "options": {
+ "raw": { "language": "json" }
+ }
+ },
+ "url": {
+ "raw": "https://api.example.com/users",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["users"]
+ }
+ }
+ },
+ {
+ "name": "API Folder",
+ "item": [
+ {
+ "name": "Valid Nested GET",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/nested/valid",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["nested", "valid"]
+ }
+ }
+ },
+ {
+ "name": "Nested Missing Method",
+ "request": {
+ "method": null,
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/nested/should-be-skipped",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["nested", "should-be-skipped"]
+ }
+ }
+ },
+ {
+ "name": "Deep Subfolder",
+ "item": [
+ {
+ "name": "Deep Valid Request",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/deep/valid",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["deep", "valid"]
+ }
+ }
+ },
+ {
+ "name": "Deep Bad Method",
+ "request": {
+ "method": null,
+ "header": [],
+ "url": {
+ "raw": "https://api.example.com/deep/should-be-skipped",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["deep", "should-be-skipped"]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Valid PUT Request",
+ "request": {
+ "method": "PUT",
+ "header": [
+ { "key": "Content-Type", "value": "application/json" },
+ { "key": "Authorization", "value": "Bearer {{token}}" }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\"name\": \"updated\"}",
+ "options": {
+ "raw": { "language": "json" }
+ }
+ },
+ "url": {
+ "raw": "https://api.example.com/users/{{userId}}",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["users", "{{userId}}"],
+ "variable": [
+ { "key": "userId", "value": "123" }
+ ]
+ }
+ }
+ },
+ {
+ "name": "No Request Object At All"
+ },
+ {
+ "name": "Valid PATCH Request",
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\"status\": \"active\"}"
+ },
+ "url": {
+ "raw": "https://api.example.com/users/1/status",
+ "protocol": "https",
+ "host": ["api", "example", "com"],
+ "path": ["users", "1", "status"]
+ }
+ }
+ }
+ ],
+ "variable": [
+ { "key": "baseUrl", "value": "https://api.example.com" },
+ { "key": "token", "value": "test-token-123" }
+ ]
+}
diff --git a/tests/import/postman/fixtures/postman-with-many-import-issues.json b/tests/import/postman/fixtures/postman-with-many-import-issues.json
new file mode 100644
index 000000000..0f7553f23
--- /dev/null
+++ b/tests/import/postman/fixtures/postman-with-many-import-issues.json
@@ -0,0 +1,70 @@
+{
+ "info": {
+ "_postman_id": "test-many-import-issues-001",
+ "name": "Many Import Issues Collection",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+ },
+ "item": [
+ {
+ "name": "Valid GET Request",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": { "raw": "https://api.example.com/users", "protocol": "https", "host": ["api", "example", "com"], "path": ["users"] }
+ }
+ },
+ { "name": "Bad Method 1", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-1", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-1"] } } },
+ { "name": "Bad Method 2", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-2", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-2"] } } },
+ { "name": "Bad Method 3", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-3", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-3"] } } },
+ { "name": "Bad Method 4", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-4", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-4"] } } },
+ { "name": "Bad Method 5", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-5", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-5"] } } },
+ { "name": "Bad Method 6", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-6", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-6"] } } },
+ { "name": "Bad Method 7", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-7", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-7"] } } },
+ { "name": "Bad Method 8", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-8", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-8"] } } },
+ { "name": "Bad Method 9", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-9", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-9"] } } },
+ { "name": "Bad Method 10", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/bad-10", "protocol": "https", "host": ["api", "example", "com"], "path": ["bad-10"] } } },
+ { "name": "Empty Method 1", "request": { "method": "", "header": [], "url": { "raw": "https://api.example.com/empty-1", "protocol": "https", "host": ["api", "example", "com"], "path": ["empty-1"] } } },
+ { "name": "Empty Method 2", "request": { "method": "", "header": [], "url": { "raw": "https://api.example.com/empty-2", "protocol": "https", "host": ["api", "example", "com"], "path": ["empty-2"] } } },
+ { "name": "Empty Method 3", "request": { "method": "", "header": [], "url": { "raw": "https://api.example.com/empty-3", "protocol": "https", "host": ["api", "example", "com"], "path": ["empty-3"] } } },
+ { "name": "Empty Method 4", "request": { "method": "", "header": [], "url": { "raw": "https://api.example.com/empty-4", "protocol": "https", "host": ["api", "example", "com"], "path": ["empty-4"] } } },
+ { "name": "Empty Method 5", "request": { "method": "", "header": [], "url": { "raw": "https://api.example.com/empty-5", "protocol": "https", "host": ["api", "example", "com"], "path": ["empty-5"] } } },
+ {
+ "name": "Valid POST Request",
+ "request": {
+ "method": "POST",
+ "header": [{ "key": "Content-Type", "value": "application/json" }],
+ "body": { "mode": "raw", "raw": "{\"name\": \"test\"}" },
+ "url": { "raw": "https://api.example.com/users", "protocol": "https", "host": ["api", "example", "com"], "path": ["users"] }
+ }
+ },
+ { "name": "Whitespace Method 1", "request": { "method": " ", "header": [], "url": { "raw": "https://api.example.com/ws-1", "protocol": "https", "host": ["api", "example", "com"], "path": ["ws-1"] } } },
+ { "name": "Whitespace Method 2", "request": { "method": " ", "header": [], "url": { "raw": "https://api.example.com/ws-2", "protocol": "https", "host": ["api", "example", "com"], "path": ["ws-2"] } } },
+ { "name": "Whitespace Method 3", "request": { "method": " ", "header": [], "url": { "raw": "https://api.example.com/ws-3", "protocol": "https", "host": ["api", "example", "com"], "path": ["ws-3"] } } },
+ { "name": "Whitespace Method 4", "request": { "method": " ", "header": [], "url": { "raw": "https://api.example.com/ws-4", "protocol": "https", "host": ["api", "example", "com"], "path": ["ws-4"] } } },
+ { "name": "Whitespace Method 5", "request": { "method": " ", "header": [], "url": { "raw": "https://api.example.com/ws-5", "protocol": "https", "host": ["api", "example", "com"], "path": ["ws-5"] } } },
+ { "name": "Missing Method 1", "request": { "header": [], "url": { "raw": "https://api.example.com/miss-1", "protocol": "https", "host": ["api", "example", "com"], "path": ["miss-1"] } } },
+ { "name": "Missing Method 2", "request": { "header": [], "url": { "raw": "https://api.example.com/miss-2", "protocol": "https", "host": ["api", "example", "com"], "path": ["miss-2"] } } },
+ { "name": "Missing Method 3", "request": { "header": [], "url": { "raw": "https://api.example.com/miss-3", "protocol": "https", "host": ["api", "example", "com"], "path": ["miss-3"] } } },
+ { "name": "Missing Method 4", "request": { "header": [], "url": { "raw": "https://api.example.com/miss-4", "protocol": "https", "host": ["api", "example", "com"], "path": ["miss-4"] } } },
+ { "name": "Missing Method 5", "request": { "header": [], "url": { "raw": "https://api.example.com/miss-5", "protocol": "https", "host": ["api", "example", "com"], "path": ["miss-5"] } } },
+ {
+ "name": "API Folder",
+ "item": [
+ { "name": "Nested Bad 1", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/nested-bad-1", "protocol": "https", "host": ["api", "example", "com"], "path": ["nested-bad-1"] } } },
+ { "name": "Nested Bad 2", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/nested-bad-2", "protocol": "https", "host": ["api", "example", "com"], "path": ["nested-bad-2"] } } },
+ { "name": "Nested Bad 3", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/nested-bad-3", "protocol": "https", "host": ["api", "example", "com"], "path": ["nested-bad-3"] } } },
+ { "name": "Nested Bad 4", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/nested-bad-4", "protocol": "https", "host": ["api", "example", "com"], "path": ["nested-bad-4"] } } },
+ { "name": "Nested Bad 5", "request": { "method": null, "header": [], "url": { "raw": "https://api.example.com/nested-bad-5", "protocol": "https", "host": ["api", "example", "com"], "path": ["nested-bad-5"] } } }
+ ]
+ },
+ {
+ "name": "Valid PUT Request",
+ "request": {
+ "method": "PUT",
+ "header": [{ "key": "Content-Type", "value": "application/json" }],
+ "body": { "mode": "raw", "raw": "{\"name\": \"updated\"}" },
+ "url": { "raw": "https://api.example.com/users/1", "protocol": "https", "host": ["api", "example", "com"], "path": ["users", "1"] }
+ }
+ }
+ ]
+}
diff --git a/tests/import/postman/import-many-issues-collection.spec.ts b/tests/import/postman/import-many-issues-collection.spec.ts
new file mode 100644
index 000000000..14cd8e957
--- /dev/null
+++ b/tests/import/postman/import-many-issues-collection.spec.ts
@@ -0,0 +1,70 @@
+import { test, expect } from '../../../playwright';
+import * as path from 'path';
+import { closeAllCollections, dismissImportIssuesToasts, importCollection } from '../../utils/page';
+import { buildCommonLocators } from '../../utils/page/locators';
+
+test.describe('Import Postman Collection with many issues (URL too long warning)', () => {
+ test.afterEach(async ({ page }) => {
+ await dismissImportIssuesToasts(page);
+ await closeAllCollections(page);
+ });
+
+ test('should show URL-too-long warning when include failed request data is checked', async ({ page, createTmpDir }) => {
+ const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-with-many-import-issues.json');
+ const tmpDir = await createTmpDir('postman-many-issues');
+ const locators = buildCommonLocators(page);
+
+ await importCollection(page, postmanFile, tmpDir, {
+ expectedCollectionName: 'Many Import Issues Collection',
+ expectIssues: true
+ });
+
+ await test.step('Verify toast title and action buttons are visible', async () => {
+ await expect(locators.import.issuesToastTitle()).toBeVisible();
+ await expect(locators.import.issuesToastTitle()).toContainText('item(s) skipped');
+ await expect(locators.import.issuesToastCopyBtn()).toBeVisible();
+ await expect(locators.import.issuesToastReportBtn()).toBeVisible();
+ });
+
+ await test.step('Check include failed request data checkbox', async () => {
+ const checkbox = locators.import.issuesToastIncludeItemsCheckbox();
+ await expect(checkbox).toBeVisible();
+ await checkbox.check();
+ await expect(checkbox).toBeChecked();
+ });
+
+ await test.step('Verify URL-too-long warning appears after checking include items', async () => {
+ const warning = locators.import.issuesToastUrlTooLongWarning();
+ await expect(warning).toBeVisible();
+ await expect(warning).toContainText('clipboard');
+ });
+
+ await test.step('Verify valid requests were imported', async () => {
+ await expect(locators.sidebar.request('Valid GET Request')).toBeVisible();
+ await expect(locators.sidebar.request('Valid POST Request')).toBeVisible();
+ await expect(locators.sidebar.request('Valid PUT Request')).toBeVisible();
+ });
+
+ await test.step('Report on GitHub copies to clipboard and opens URL without body', async () => {
+ await page.evaluate(() => {
+ (window as any).__capturedOpenUrl = null;
+ window.open = (url?: string | URL) => {
+ (window as any).__capturedOpenUrl = url != null ? String(url) : '';
+ return null;
+ };
+ });
+
+ await locators.import.issuesToastReportBtn().click();
+
+ // Should show clipboard success toast
+ await expect(page.getByText('Issue details copied')).toBeVisible({ timeout: 3000 });
+
+ // URL should have title but NOT body (since it was too long)
+ const openedUrl = await page.evaluate(() => (window as any).__capturedOpenUrl as string);
+ expect(openedUrl).toContain('https://github.com/usebruno/bruno/issues/new');
+ expect(openedUrl).toContain('title=');
+ expect(openedUrl).toContain('labels=bug');
+ expect(openedUrl).not.toContain('Missing+or+invalid+request+method');
+ });
+ });
+});
diff --git a/tests/import/postman/import-partial-collection.spec.ts b/tests/import/postman/import-partial-collection.spec.ts
new file mode 100644
index 000000000..fb0c08d20
--- /dev/null
+++ b/tests/import/postman/import-partial-collection.spec.ts
@@ -0,0 +1,118 @@
+import { test, expect } from '../../../playwright';
+import * as path from 'path';
+import { closeAllCollections, dismissImportIssuesToasts, importCollection } from '../../utils/page';
+import { buildCommonLocators } from '../../utils/page/locators';
+
+test.describe('Import Postman Collection with partial import issues', () => {
+ test.afterEach(async ({ page }) => {
+ await dismissImportIssuesToasts(page);
+ await closeAllCollections(page);
+ });
+
+ test('should import valid requests and show issues toast for skipped items', async ({ page, createTmpDir }) => {
+ const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-with-import-issues.json');
+ const tmpDir = await createTmpDir('postman-partial-import');
+ const locators = buildCommonLocators(page);
+
+ await importCollection(page, postmanFile, tmpDir, {
+ expectedCollectionName: 'Import Issues Test Collection',
+ expectIssues: true
+ });
+
+ await test.step('Verify import issues toast content', async () => {
+ const toastTitle = locators.import.issuesToastTitle();
+ await expect(toastTitle).toBeVisible();
+ await expect(toastTitle).toContainText('item(s) skipped');
+ });
+
+ await test.step('Verify toast action buttons are visible', async () => {
+ await expect(locators.import.issuesToastCopyBtn()).toBeVisible();
+ await expect(locators.import.issuesToastReportBtn()).toBeVisible();
+ });
+
+ await test.step('Verify include items checkbox is visible', async () => {
+ await expect(locators.import.issuesToastIncludeItemsCheckbox()).toBeVisible();
+ });
+
+ await test.step('Verify valid top-level requests were imported', async () => {
+ await expect(locators.sidebar.request('Valid GET Request')).toBeVisible();
+ await expect(locators.sidebar.request('Valid POST Request')).toBeVisible();
+ await expect(locators.sidebar.request('Valid PUT Request')).toBeVisible();
+ await expect(locators.sidebar.request('Valid PATCH Request')).toBeVisible();
+ });
+
+ await test.step('Verify skipped requests are NOT in the sidebar', async () => {
+ await expect(locators.sidebar.request('Missing Method (null)')).not.toBeVisible();
+ await expect(locators.sidebar.request('Missing Method (absent)')).not.toBeVisible();
+ await expect(locators.sidebar.request('Empty String Method')).not.toBeVisible();
+ await expect(locators.sidebar.request('Whitespace-Only Method')).not.toBeVisible();
+ });
+
+ await test.step('Verify folder and nested valid requests were imported', async () => {
+ const folder = locators.sidebar.folder('API Folder');
+ await expect(folder).toBeVisible();
+ await folder.click();
+
+ await expect(locators.sidebar.request('Valid Nested GET')).toBeVisible();
+
+ const subfolder = locators.sidebar.folder('Deep Subfolder');
+ await expect(subfolder).toBeVisible();
+ await subfolder.click();
+
+ await expect(locators.sidebar.request('Deep Valid Request')).toBeVisible();
+ });
+
+ await test.step('Verify nested skipped requests are NOT in the sidebar', async () => {
+ await expect(locators.sidebar.request('Nested Missing Method')).not.toBeVisible();
+ await expect(locators.sidebar.request('Deep Bad Method')).not.toBeVisible();
+ });
+ });
+
+ test('should allow copying import issues to clipboard', async ({ page, createTmpDir }) => {
+ const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-with-import-issues.json');
+ const tmpDir = await createTmpDir('postman-partial-import-copy');
+ const locators = buildCommonLocators(page);
+
+ await importCollection(page, postmanFile, tmpDir, {
+ expectedCollectionName: 'Import Issues Test Collection',
+ expectIssues: true
+ });
+
+ await test.step('Click copy button and verify success toast', async () => {
+ await locators.import.issuesToastCopyBtn().click();
+ await expect(page.getByText('Copied to clipboard')).toBeVisible({ timeout: 3000 });
+ });
+ });
+
+ test('should open GitHub issue with prefilled details when clicking Report on GitHub', async ({ page, createTmpDir }) => {
+ const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-with-import-issues.json');
+ const tmpDir = await createTmpDir('postman-partial-import-report');
+ const locators = buildCommonLocators(page);
+
+ await importCollection(page, postmanFile, tmpDir, {
+ expectedCollectionName: 'Import Issues Test Collection',
+ expectIssues: true
+ });
+
+ await test.step('Mock window.open and click Report on GitHub', async () => {
+ // Mock window.open to capture the URL instead of opening a browser
+ await page.evaluate(() => {
+ (window as any).__capturedOpenUrl = null;
+ window.open = (url?: string | URL) => {
+ (window as any).__capturedOpenUrl = url != null ? String(url) : '';
+ return null;
+ };
+ });
+
+ await locators.import.issuesToastReportBtn().click();
+
+ const openedUrl = await page.evaluate(() => (window as any).__capturedOpenUrl as string);
+
+ expect(openedUrl).toContain('https://github.com/usebruno/bruno/issues/new');
+ expect(openedUrl).toContain('title=');
+ expect(openedUrl).toContain('Postman+import');
+ expect(openedUrl).toContain('labels=bug');
+ expect(openedUrl).toContain('Missing+or+invalid+request+method');
+ });
+ });
+});
diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts
index 11f14cb20..f3605d979 100644
--- a/tests/utils/page/actions.ts
+++ b/tests/utils/page/actions.ts
@@ -18,6 +18,22 @@ const waitForReadyPage = (
options: WaitForAppReadyOptions = {}
) => waitForReadyPageImpl(app, options);
+/**
+ * Dismiss all import issues toasts (they use infinite duration and persist across tests).
+ * @param page - The page object
+ * @returns void
+ */
+const dismissImportIssuesToasts = async (page: Page) => {
+ await test.step('Dismiss import issues toasts', async () => {
+ const toasts = page.getByTestId('import-issues-toast');
+ while (await toasts.count() > 0) {
+ const toast = toasts.first();
+ await toast.getByTestId('import-issues-toast-close').click();
+ await expect(toast).not.toBeVisible({ timeout: 5000 });
+ }
+ });
+};
+
/**
* Close all collections
* @param page - The page object
@@ -428,6 +444,7 @@ const deleteCollectionFromOverview = async (page: Page, collectionName: string)
*/
type ImportCollectionOptions = {
expectedCollectionName?: string;
+ expectIssues?: boolean;
};
const importCollection = async (
@@ -471,6 +488,11 @@ const importCollection = async (
).toBeVisible();
}
+ // Wait for import issues toast if expected
+ if (options.expectIssues) {
+ await expect(locators.import.issuesToast()).toBeVisible({ timeout: 10000 });
+ }
+
if (options.expectedCollectionName) {
await openCollection(page, options.expectedCollectionName);
}
@@ -1546,6 +1568,7 @@ const openWorkspaceFromDialog = async (app: any, page: any, targetPath: string)
export {
waitForReadyPage,
+ dismissImportIssuesToasts,
closeAllCollections,
openCollection,
createCollection,
diff --git a/tests/utils/page/locators.ts b/tests/utils/page/locators.ts
index 4dda96a4f..7a2d4c4c1 100644
--- a/tests/utils/page/locators.ts
+++ b/tests/utils/page/locators.ts
@@ -129,7 +129,19 @@ export const buildCommonLocators = (page: Page) => ({
envOption: (name: string) => page.locator('.dropdown-item').getByText(name, { exact: true }),
parsingError: () => page.getByTestId('import-error-message'),
browseLink: (root?: Locator) => (root ?? page).getByTestId('import-collection-browse-link'),
- importButton: (root?: Locator) => (root ?? page).getByTestId('import-collection-location-modal-submit-btn')
+ importButton: (root?: Locator) => (root ?? page).getByTestId('import-collection-location-modal-submit-btn'),
+ ...(() => {
+ const issuesToast = () => page.getByTestId('import-issues-toast').last();
+ return {
+ issuesToast,
+ issuesToastTitle: () => issuesToast().getByTestId('import-issues-toast-title'),
+ issuesToastCopyBtn: () => issuesToast().getByTestId('import-issues-copy-btn'),
+ issuesToastReportBtn: () => issuesToast().getByTestId('import-issues-report-btn'),
+ issuesToastIncludeItemsCheckbox: () => issuesToast().getByTestId('import-issues-include-items-checkbox'),
+ issuesToastCloseBtn: () => issuesToast().getByTestId('import-issues-toast-close'),
+ issuesToastUrlTooLongWarning: () => issuesToast().getByTestId('import-issues-url-too-long-warning')
+ };
+ })()
},
/**
* Build generic table locators for any table with a testId