mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
feat(import): enhance import functionality with issue tracking and logging (#8098)
* feat: enhance import functionality with issue tracking and logging - Updated the import process to return both collections and issues for better error handling. - Introduced a new toast notification for displaying import issues, allowing users to copy or report them. - Enhanced logging for import issues, capturing errors and warnings during the import process. - Added new components for actionable toasts and import issues display. - Updated tests to validate the new import behavior and issue tracking. * feat: enhance import issues handling with new toast notifications and tests - Added optional testId prop to ActionableToast for better test targeting. - Updated ImportIssuesToast to include data-testid attributes for improved e2e testing. - Introduced a new Postman collection fixture to test partial import scenarios. - Created new tests to validate the import process, including issue reporting and copying functionality. - Implemented utility functions to manage import issues toasts during tests. * fix: improve clipboard copy functionality and handle import issues more robustly - Updated BulkImportCollectionLocation to always set import issues, ensuring consistent state management. - Enhanced clipboard copy functionality in ImportIssuesToast and BulkImportCollectionLocation to handle errors gracefully with user feedback. - Added aria-label for better accessibility in ActionableToast close button. * refactor: enhance import issue logging and toast notifications - Improved logging in BulkImportCollectionLocation and ImportCollectionLocation to provide detailed summaries of import issues, including counts of skipped items and warnings. - Updated ImportIssuesToast to handle long issue descriptions and provide user feedback for copying issue details to the clipboard. - Removed ActionableToast component and its styles, consolidating toast functionality within ImportIssuesToast for better maintainability. - Enhanced styling for ImportIssuesToast to improve user experience and accessibility. * refactor: update logging level for import issues in BulkImportCollectionLocation and ImportCollectionLocation - Changed log type from 'error' to 'warn' for import issue summaries in both components to better reflect the severity of the messages. - This adjustment improves clarity in the logging system and aligns with the intended handling of import warnings. * feat: enhance ImportIssuesToast with URL length warning and styling improvements - Added an alert icon and improved styling for the URL-too-long warning in ImportIssuesToast to enhance user experience. - Introduced a new test for verifying the display of the URL length warning when importing collections with many issues. - Updated locators to include a test ID for the URL-too-long warning, facilitating better end-to-end testing. * style: update ImportIssuesToast styling for improved user experience - Changed background and border colors in StyledWrapper for better visual consistency. - Enhanced box-shadow and close button styles for improved accessibility and interaction. - Adjusted padding and gap in warning messages for better layout and readability.
This commit is contained in:
@@ -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
|
||||
</button>
|
||||
)}
|
||||
{status[collection.uid] === STATUS.SUCCESS && importIssues[collection.uid] && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-yellow-600 text-xs">
|
||||
{importIssues[collection.uid].filter((i) => i.severity === 'error').length} item(s) skipped
|
||||
</span>
|
||||
<button
|
||||
onClick={async () => {
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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 }));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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 (
|
||||
<StyledWrapper
|
||||
data-testid="import-issues-toast"
|
||||
style={{
|
||||
opacity: t.visible ? 1 : 0,
|
||||
transform: t.visible ? 'translateX(0)' : 'translateX(100%)'
|
||||
}}
|
||||
>
|
||||
<div className="toast-accent" />
|
||||
<div className="toast-body">
|
||||
<button
|
||||
type="button"
|
||||
className="toast-close"
|
||||
aria-label="Close toast"
|
||||
data-testid="import-issues-toast-close"
|
||||
onClick={() => toast.dismiss(t.id)}
|
||||
>
|
||||
<IconX size={14} />
|
||||
</button>
|
||||
<div className="toast-title" data-testid="import-issues-toast-title">Imported with issues: {summary}</div>
|
||||
<div className="toast-hint">Open DevTools console to see which items failed and why.</div>
|
||||
{hasSourceItems && (
|
||||
<label className="toast-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeItems}
|
||||
onChange={(e) => setIncludeItems(e.target.checked)}
|
||||
data-testid="import-issues-include-items-checkbox"
|
||||
/>
|
||||
<div className="toast-checkbox-text">
|
||||
<span className="toast-checkbox-label">Include failed request data</span>
|
||||
<span className="toast-checkbox-desc">Attaches the raw Postman request items that failed. May contain API keys, tokens, or internal URLs.</span>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
{isUrlTooLong && (
|
||||
<div className="toast-warning" data-testid="import-issues-url-too-long-warning">
|
||||
<IconAlertCircle size={14} className="toast-warning-icon" />
|
||||
<span>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.</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="toast-actions">
|
||||
<button className="toast-btn" onClick={handleReport} data-testid="import-issues-report-btn">
|
||||
<IconBrandGithub size={13} />
|
||||
Report on GitHub
|
||||
</button>
|
||||
<button className="toast-btn" onClick={handleCopy} data-testid="import-issues-copy-btn">
|
||||
<IconCopy size={13} />
|
||||
Copy Issues
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) => (
|
||||
<ImportIssuesToastContent t={t} issues={issues} summary={summary} />
|
||||
),
|
||||
{ duration: Infinity, position: 'bottom-right' }
|
||||
);
|
||||
|
||||
return activeImportToastId;
|
||||
};
|
||||
|
||||
export default ImportIssuesToastContent;
|
||||
@@ -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}`);
|
||||
|
||||
210
packages/bruno-converters/tests/fixtures/postman-with-import-issues.json
vendored
Normal file
210
packages/bruno-converters/tests/fixtures/postman-with-import-issues.json
vendored
Normal file
@@ -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" }
|
||||
]
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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: !@#$%^&*()');
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
210
tests/import/postman/fixtures/postman-with-import-issues.json
Normal file
210
tests/import/postman/fixtures/postman-with-import-issues.json
Normal file
@@ -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" }
|
||||
]
|
||||
}
|
||||
@@ -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"] }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
70
tests/import/postman/import-many-issues-collection.spec.ts
Normal file
70
tests/import/postman/import-many-issues-collection.spec.ts
Normal file
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
118
tests/import/postman/import-partial-collection.spec.ts
Normal file
118
tests/import/postman/import-partial-collection.spec.ts
Normal file
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user