mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
7 Commits
feat/node_
...
release/v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
518b1ed441 | ||
|
|
c1aa682c03 | ||
|
|
01275acc89 | ||
|
|
c8f223a000 | ||
|
|
4202b48edd | ||
|
|
69891c0bc7 | ||
|
|
76729519c6 |
Binary file not shown.
|
Before Width: | Height: | Size: 813 KiB After Width: | Height: | Size: 537 KiB |
@@ -1,25 +0,0 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
|
||||
test.describe('Notifications Modal', () => {
|
||||
test('should open notifications modal when clicking bell icon and close with close button', async ({ page }) => {
|
||||
// Get the notification bell icon in the status bar
|
||||
const notificationBell = page.getByLabel('Check all Notifications');
|
||||
|
||||
// Click on the bell icon to open notifications
|
||||
await notificationBell.click();
|
||||
|
||||
// Get modal elements
|
||||
const notificationsModal = page.locator('.bruno-modal');
|
||||
const modalCloseButton = notificationsModal.locator('div.bruno-modal-header div.close');
|
||||
|
||||
// Verify modal is visible and has the correct title
|
||||
await expect(notificationsModal).toBeVisible();
|
||||
await expect(notificationsModal.locator('.bruno-modal-header-title')).toContainText('NOTIFICATIONS');
|
||||
|
||||
// Click the close button
|
||||
await modalCloseButton.click();
|
||||
|
||||
// Verify modal is closed
|
||||
await expect(notificationsModal).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
|
||||
test.describe('Sidebar Toggle', () => {
|
||||
test('should toggle sidebar visibility when clicking the toggle button', async ({ page }) => {
|
||||
// Get the sidebar and toggle button elements
|
||||
const sidebar = page.locator('aside.sidebar');
|
||||
const toggleButton = page.getByLabel('Toggle Sidebar');
|
||||
const dragHandle = page.locator('.sidebar-drag-handle');
|
||||
|
||||
// Initial state - sidebar and drag handle should be visible
|
||||
await expect(sidebar).toBeVisible();
|
||||
await expect(dragHandle).toBeVisible();
|
||||
|
||||
// Click toggle to hide sidebar
|
||||
await toggleButton.click();
|
||||
|
||||
// Wait for transition to complete and verify sidebar and drag handle are hidden
|
||||
await expect(sidebar).not.toBeVisible();
|
||||
await expect(dragHandle).not.toBeVisible();
|
||||
|
||||
// Verify the sidebar has collapsed width
|
||||
const sidebarBox = await sidebar.boundingBox();
|
||||
expect(sidebarBox?.width).toBe(0);
|
||||
|
||||
// Click toggle again to show sidebar
|
||||
await toggleButton.click();
|
||||
|
||||
// Wait for transition and verify sidebar and drag handle are visible again
|
||||
await expect(sidebar).toBeVisible();
|
||||
await expect(dragHandle).toBeVisible();
|
||||
|
||||
// Verify the sidebar has expanded width
|
||||
const expandedSidebarBox = await sidebar.boundingBox();
|
||||
expect(expandedSidebarBox?.width).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -186,8 +186,6 @@ export default class CodeEditor extends React.Component {
|
||||
if (editor) {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
editor.on('change', this._onEdit);
|
||||
editor.on('scroll', this.onScroll);
|
||||
editor.scrollTo(null, this.props.initialScroll);
|
||||
this.addOverlay();
|
||||
|
||||
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
|
||||
@@ -232,18 +230,12 @@ export default class CodeEditor extends React.Component {
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
|
||||
if (this.props.initialScroll !== prevProps.initialScroll) {
|
||||
this.editor.scrollTo(null, this.props.initialScroll);
|
||||
}
|
||||
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor.off('scroll', this.onScroll);
|
||||
this.editor = null;
|
||||
}
|
||||
|
||||
@@ -279,8 +271,6 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
onScroll = (event) => this.props.onScroll?.(event);
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
|
||||
@@ -67,10 +67,10 @@ const RequestTab = ({ request, response }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{request?.data && (
|
||||
{request?.body && (
|
||||
<div className="section">
|
||||
<h4>Request Body</h4>
|
||||
<pre className="code-block">{formatBody(request.data)}</pre>
|
||||
<pre className="code-block">{formatBody(request.body)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -239,4 +239,4 @@ const RequestDetailsPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestDetailsPanel;
|
||||
export default RequestDetailsPanel;
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const IconSidebarToggle = ({ collapsed = false, size = 16, strokeWidth = 1.5, className = '', ...rest }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={`icon icon-tabler icons-tabler-outline icon-tabler-layout-sidebar ${className}`}
|
||||
{...rest}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" />
|
||||
<path d="M9 4l0 16" />
|
||||
{!collapsed && (
|
||||
<rect x="4.6" y="4.6" width="4.8" height="14.8" rx="0.8" fill="currentColor" />
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconSidebarToggle;
|
||||
@@ -79,7 +79,7 @@ const Notifications = () => {
|
||||
|
||||
const modalCustomHeader = (
|
||||
<div className="flex flex-row gap-8">
|
||||
<div className="bruno-modal-header-title">NOTIFICATIONS</div>
|
||||
<div>NOTIFICATIONS</div>
|
||||
{unreadNotifications.length > 0 && (
|
||||
<>
|
||||
<div className="normal-case font-normal">
|
||||
|
||||
@@ -79,7 +79,7 @@ const Beta = ({ close }) => {
|
||||
<h2 className="text-lg font-semibold">Beta Features</h2>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4 text-wrap">
|
||||
Beta features are experimental previews that may change before full release. Try them and share feedback.
|
||||
Enable beta features, these features may be unstable or incomplete.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -98,16 +98,6 @@ const Beta = ({ close }) => {
|
||||
<label className="block ml-2 select-none font-medium" htmlFor={feature.id}>
|
||||
{feature.label}
|
||||
</label>
|
||||
{feature.id === 'grpc' && (
|
||||
<a
|
||||
href="https://github.com/usebruno/bruno/discussions/5447"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-2 text-xs text-blue-500 hover:text-blue-600 underline"
|
||||
>
|
||||
Share feedback
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="beta-feature-description ml-6 text-xs text-gray-500 dark:text-gray-400">
|
||||
{feature.description}
|
||||
|
||||
@@ -147,7 +147,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
<Table
|
||||
headers={[
|
||||
{ name: 'Name', accessor: 'name', width: '31%' },
|
||||
{ name: 'Value', accessor: 'path', width: '56%' },
|
||||
{ name: 'Path', accessor: 'path', width: '56%' },
|
||||
{ name: '', accessor: '', width: '13%' }
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -18,7 +18,6 @@ const RequestTabs = () => {
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const screenWidth = useSelector((state) => state.app.screenWidth);
|
||||
|
||||
const getTabClassname = (tab, index) => {
|
||||
@@ -50,8 +49,7 @@ const RequestTabs = () => {
|
||||
const activeCollection = find(collections, (c) => c.uid === activeTab.collectionUid);
|
||||
const collectionRequestTabs = filter(tabs, (t) => t.collectionUid === activeTab.collectionUid);
|
||||
|
||||
const effectiveSidebarWidth = sidebarCollapsed ? 0 : leftSidebarWidth;
|
||||
const maxTablistWidth = screenWidth - effectiveSidebarWidth - 150;
|
||||
const maxTablistWidth = screenWidth - leftSidebarWidth - 150;
|
||||
const tabsWidth = collectionRequestTabs.length * 150 + 34; // 34: (+)icon
|
||||
const showChevrons = maxTablistWidth < tabsWidth;
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import { get } from 'lodash';
|
||||
import find from 'lodash/find';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateResponsePaneScrollPosition } from 'providers/ReduxStore/slices/tabs';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { Document, Page } from 'react-pdf';
|
||||
import 'pdfjs-dist/build/pdf.worker';
|
||||
@@ -53,10 +51,6 @@ const QueryResultPreview = ({
|
||||
displayedTheme
|
||||
}) => {
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [numPages, setNumPages] = useState(null);
|
||||
@@ -72,19 +66,9 @@ const QueryResultPreview = ({
|
||||
if (disableRunEventListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
const onScroll = (event) => {
|
||||
dispatch(
|
||||
updateResponsePaneScrollPosition({
|
||||
uid: focusedTab.uid,
|
||||
scrollY: event.doc.scrollTop
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
switch (previewTab?.mode) {
|
||||
case 'preview-web': {
|
||||
const webViewSrc = data.replace('<head>', `<head><base href="${item.requestSent?.url || ''}">`);
|
||||
@@ -127,10 +111,8 @@ const QueryResultPreview = ({
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
theme={displayedTheme}
|
||||
onRun={onRun}
|
||||
onScroll={onScroll}
|
||||
value={formattedData}
|
||||
mode={mode}
|
||||
initialScroll={focusedTab.responsePaneScrollPosition || 0}
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -116,7 +116,7 @@ export default function RunnerResults({ collection }) {
|
||||
displayName: getDisplayName(collection.pathname, info.pathname, info.name),
|
||||
tags: [...(info.request?.tags || [])].sort(),
|
||||
};
|
||||
if (newItem.status !== 'error' && newItem.status !== 'skipped' && newItem.status !== 'running') {
|
||||
if (newItem.status !== 'error' && newItem.status !== 'skipped') {
|
||||
newItem.testStatus = getTestStatus(newItem.testResults);
|
||||
newItem.assertionStatus = getTestStatus(newItem.assertionResults);
|
||||
newItem.preRequestTestStatus = getTestStatus(newItem.preRequestTestResults);
|
||||
|
||||
@@ -30,9 +30,8 @@ export const CollectionItemDragPreview = () => {
|
||||
clientOffset: monitor.getClientOffset(),
|
||||
}));
|
||||
if (!isDragging) return null;
|
||||
if (!item.type) return null;
|
||||
const { x, y } = clientOffset || {};
|
||||
const shouldShowFolderIcon = item.type === 'folder';
|
||||
const shouldShowFolderIcon = !item.type || item.type === 'folder';
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div style={getItemStyles({ x, y })} className='p-2'>
|
||||
|
||||
@@ -13,7 +13,6 @@ import { IconArrowBackUp, IconEdit } from '@tabler/icons';
|
||||
import Help from 'components/Help';
|
||||
import { multiLineMsg } from "utils/common";
|
||||
import { formatIpcError } from "utils/common/error";
|
||||
import { toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
const CreateCollection = ({ onClose }) => {
|
||||
const inputRef = useRef();
|
||||
@@ -46,7 +45,6 @@ const CreateCollection = ({ onClose }) => {
|
||||
dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation))
|
||||
.then(() => {
|
||||
toast.success('Collection created!');
|
||||
dispatch(toggleSidebarCollapse());
|
||||
onClose();
|
||||
})
|
||||
.catch((e) => toast.error(multiLineMsg('An error occurred while creating the collection', formatIpcError(e))));
|
||||
|
||||
@@ -1,204 +1,126 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { IconLoader2, IconFileImport } from '@tabler/icons';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { IconLoader2 } from '@tabler/icons';
|
||||
import importBrunoCollection from 'utils/importers/bruno-collection';
|
||||
import { postmanToBruno, readFile } from 'utils/importers/postman-collection';
|
||||
import importInsomniaCollection from 'utils/importers/insomnia-collection';
|
||||
import importOpenapiCollection from 'utils/importers/openapi-collection';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { postmanToBruno, isPostmanCollection } from 'utils/importers/postman-collection';
|
||||
import { convertInsomniaToBruno, isInsomniaCollection } from 'utils/importers/insomnia-collection';
|
||||
import { convertOpenapiToBruno, isOpenApiSpec } from 'utils/importers/openapi-collection';
|
||||
import { processBrunoCollection } from 'utils/importers/bruno-collection';
|
||||
|
||||
const convertFileToObject = async (file) => {
|
||||
const text = await file.text();
|
||||
|
||||
try {
|
||||
if (file.type === 'application/json' || file.name.endsWith('.json')) {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
const parsed = jsyaml.load(text);
|
||||
if (typeof parsed !== 'object' || parsed === null) {
|
||||
throw new Error();
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
throw new Error('Failed to parse the file – ensure it is valid JSON or YAML');
|
||||
}
|
||||
};
|
||||
|
||||
const FullscreenLoader = ({ isLoading }) => {
|
||||
const [loadingMessage, setLoadingMessage] = useState('');
|
||||
|
||||
// Messages to cycle through while loading
|
||||
const loadingMessages = [
|
||||
'Processing collection...',
|
||||
'Analyzing requests...',
|
||||
'Translating scripts...',
|
||||
'Preparing collection...',
|
||||
'Almost done...'
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) return;
|
||||
|
||||
let messageIndex = 0;
|
||||
const interval = setInterval(() => {
|
||||
messageIndex = (messageIndex + 1) % loadingMessages.length;
|
||||
setLoadingMessage(loadingMessages[messageIndex]);
|
||||
}, 2000);
|
||||
|
||||
setLoadingMessage(loadingMessages[0]);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isLoading]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm transition-all duration-300">
|
||||
<div className="flex flex-col items-center p-8 rounded-lg bg-white dark:bg-zinc-800 shadow-lg max-w-md text-center">
|
||||
<IconLoader2 className="animate-spin h-12 w-12 mb-4" strokeWidth={1.5} />
|
||||
<h3 className="text-lg font-medium text-zinc-900 dark:text-zinc-50 mb-2">
|
||||
{loadingMessage}
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
This may take a moment depending on the collection size
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import fileDialog from 'file-dialog';
|
||||
|
||||
const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const fileInputRef = useRef(null);
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleDrag = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
if (e.type === 'dragenter' || e.type === 'dragover') {
|
||||
setDragActive(true);
|
||||
} else if (e.type === 'dragleave') {
|
||||
setDragActive(false);
|
||||
}
|
||||
const handleImportBrunoCollection = () => {
|
||||
importBrunoCollection()
|
||||
.then(({ collection }) => {
|
||||
handleSubmit({ collection });
|
||||
})
|
||||
.catch((err) => toastError(err, 'Import collection failed'))
|
||||
};
|
||||
|
||||
const processFile = async (file) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await convertFileToObject(file);
|
||||
|
||||
if (!data) {
|
||||
throw new Error('Failed to parse file content');
|
||||
}
|
||||
|
||||
let collection;
|
||||
|
||||
if (isPostmanCollection(data)) {
|
||||
collection = await postmanToBruno(data);
|
||||
}
|
||||
else if (isInsomniaCollection(data)) {
|
||||
collection = convertInsomniaToBruno(data);
|
||||
}
|
||||
else if (isOpenApiSpec(data)) {
|
||||
collection = convertOpenapiToBruno(data);
|
||||
}
|
||||
else {
|
||||
collection = await processBrunoCollection(data);
|
||||
}
|
||||
|
||||
handleSubmit({ collection });
|
||||
} catch (err) {
|
||||
toastError(err, 'Import collection failed');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
|
||||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||
await processFile(e.dataTransfer.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBrowseFiles = () => {
|
||||
fileInputRef.current.click();
|
||||
};
|
||||
|
||||
const handleFileInputChange = async (e) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
await processFile(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <FullscreenLoader isLoading={isLoading} />;
|
||||
const handleImportPostmanCollection = () => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then((...args) => {
|
||||
setIsLoading(true);
|
||||
return readFile(...args);
|
||||
})
|
||||
.then((collection) => postmanToBruno(collection))
|
||||
.then((collection) => handleSubmit({ collection }))
|
||||
.catch((err) => toastError(err, 'Postman Import collection failed'))
|
||||
.finally(() => setIsLoading(false));
|
||||
}
|
||||
|
||||
const acceptedFileTypes = [
|
||||
'.json',
|
||||
'.yaml',
|
||||
'.yml',
|
||||
'application/json',
|
||||
'application/yaml',
|
||||
'application/x-yaml'
|
||||
]
|
||||
const handleImportInsomniaCollection = () => {
|
||||
importInsomniaCollection()
|
||||
.then(({ collection }) => {
|
||||
handleSubmit({ collection });
|
||||
})
|
||||
.catch((err) => toastError(err, 'Insomnia Import collection failed'))
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose}>
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Import from file</h3>
|
||||
<div
|
||||
onDragEnter={handleDrag}
|
||||
onDragOver={handleDrag}
|
||||
onDragLeave={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
className={`
|
||||
border-2 border-dashed rounded-lg p-6 transition-colors duration-200
|
||||
${dragActive
|
||||
? 'border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<IconFileImport
|
||||
size={28}
|
||||
className="text-gray-400 dark:text-gray-500 mb-3"
|
||||
/>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={handleFileInputChange}
|
||||
accept={acceptedFileTypes.join(',')}
|
||||
/>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
|
||||
Drop file to import or{' '}
|
||||
<button
|
||||
className="text-blue-500 underline cursor-pointer"
|
||||
onClick={handleBrowseFiles}
|
||||
>
|
||||
choose a file
|
||||
</button>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Supports Bruno, Postman, Insomnia, and OpenAPI v3 formats
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
const handleImportOpenapiCollection = () => {
|
||||
importOpenapiCollection()
|
||||
.then(({ collection }) => {
|
||||
handleSubmit({ collection });
|
||||
})
|
||||
.catch((err) => toastError(err, 'OpenAPI v3 Import collection failed'))
|
||||
};
|
||||
|
||||
const CollectionButton = ({ children, className, onClick }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`rounded bg-transparent px-2.5 py-1 text-xs font-semibold text-zinc-900 dark:text-zinc-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
|
||||
${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const FullscreenLoader = () => {
|
||||
const [loadingMessage, setLoadingMessage] = useState('');
|
||||
|
||||
// Messages to cycle through while loading
|
||||
const loadingMessages = [
|
||||
'Processing collection...',
|
||||
'Analyzing requests...',
|
||||
'Translating scripts...',
|
||||
'Preparing collection...',
|
||||
'Almost done...'
|
||||
];
|
||||
|
||||
|
||||
// Cycle through loading messages for better UX
|
||||
useEffect(() => {
|
||||
if (!isLoading) return;
|
||||
|
||||
let messageIndex = 0;
|
||||
const interval = setInterval(() => {
|
||||
messageIndex = (messageIndex + 1) % loadingMessages.length;
|
||||
setLoadingMessage(loadingMessages[messageIndex]);
|
||||
}, 2000);
|
||||
|
||||
setLoadingMessage(loadingMessages[0]);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isLoading]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm transition-all duration-300">
|
||||
<div className="flex flex-col items-center p-8 rounded-lg bg-white dark:bg-zinc-800 shadow-lg max-w-md text-center">
|
||||
<IconLoader2 className="animate-spin h-12 w-12 mb-4" strokeWidth={1.5} />
|
||||
<h3 className="text-lg font-medium text-zinc-900 dark:text-zinc-50 mb-2">
|
||||
{loadingMessage}
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
This may take a moment depending on the collection size
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <FullscreenLoader />}
|
||||
{!isLoading && (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose}>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-sm">Select the type of your existing collection :</h3>
|
||||
<div className="mt-4 grid grid-rows-2 grid-flow-col gap-2">
|
||||
<CollectionButton onClick={handleImportBrunoCollection}>Bruno Collection</CollectionButton>
|
||||
<CollectionButton onClick={handleImportPostmanCollection}>Postman Collection</CollectionButton>
|
||||
<CollectionButton onClick={handleImportInsomniaCollection}>Insomnia Collection</CollectionButton>
|
||||
<CollectionButton onClick={handleImportOpenapiCollection}>OpenAPI V3 Spec</CollectionButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ const Wrapper = styled.div`
|
||||
|
||||
aside {
|
||||
background-color: ${(props) => props.theme.sidebar.bg};
|
||||
overflow: hidden;
|
||||
|
||||
.collection-title {
|
||||
line-height: 1.5;
|
||||
@@ -42,7 +41,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
div.sidebar-drag-handle {
|
||||
div.drag-sidebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -51,7 +50,6 @@ const Wrapper = styled.div`
|
||||
background-color: transparent;
|
||||
width: 6px;
|
||||
right: -3px;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover div.drag-request-border {
|
||||
width: 2px;
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
import TitleBar from './TitleBar';
|
||||
import Collections from './Collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useApp } from 'providers/App';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateLeftSidebarWidth, updateIsDragging } from 'providers/ReduxStore/slices/app';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const MIN_LEFT_SIDEBAR_WIDTH = 221;
|
||||
const MAX_LEFT_SIDEBAR_WIDTH = 600;
|
||||
|
||||
const Sidebar = () => {
|
||||
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const { version } = useApp();
|
||||
const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);
|
||||
const lastWidthRef = useRef(leftSidebarWidth);
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
const currentWidth = sidebarCollapsed ? 0 : asideWidth;
|
||||
|
||||
// Clamp helper keeps width in allowed range
|
||||
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (!dragging || sidebarCollapsed) return;
|
||||
e.preventDefault();
|
||||
const nextWidth = clamp(e.clientX + 2, MIN_LEFT_SIDEBAR_WIDTH, MAX_LEFT_SIDEBAR_WIDTH);
|
||||
if (Math.abs(nextWidth - lastWidthRef.current) < 3) return;
|
||||
lastWidthRef.current = nextWidth;
|
||||
setAsideWidth(nextWidth);
|
||||
if (dragging) {
|
||||
e.preventDefault();
|
||||
let width = e.clientX + 2;
|
||||
if (width < MIN_LEFT_SIDEBAR_WIDTH || width > MAX_LEFT_SIDEBAR_WIDTH) {
|
||||
return;
|
||||
}
|
||||
setAsideWidth(width);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = (e) => {
|
||||
if (dragging) {
|
||||
e.preventDefault();
|
||||
@@ -50,9 +49,6 @@ const Sidebar = () => {
|
||||
};
|
||||
const handleDragbarMouseDown = (e) => {
|
||||
e.preventDefault();
|
||||
if (sidebarCollapsed) {
|
||||
return;
|
||||
}
|
||||
setDragging(true);
|
||||
dispatch(
|
||||
updateIsDragging({
|
||||
@@ -77,7 +73,7 @@ const Sidebar = () => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex relative h-full">
|
||||
<aside className="sidebar" style={{ width: currentWidth, transition: dragging ? 'none' : 'width 0.2s ease-in-out' }}>
|
||||
<aside>
|
||||
<div className="flex flex-row h-full w-full">
|
||||
<div className="flex flex-col w-full" style={{ width: asideWidth }}>
|
||||
<div className="flex flex-col flex-grow">
|
||||
@@ -88,11 +84,9 @@ const Sidebar = () => {
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{!sidebarCollapsed && (
|
||||
<div className="absolute sidebar-drag-handle h-full" onMouseDown={handleDragbarMouseDown}>
|
||||
<div className="drag-request-border" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute drag-sidebar h-full" onMouseDown={handleDragbarMouseDown}>
|
||||
<div className="drag-request-border" />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconSettings, IconCookie, IconTool } from '@tabler/icons';
|
||||
import IconSidebarToggle from 'components/Icons/IconSidebarToggle';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import Preferences from 'components/Preferences';
|
||||
import Cookies from 'components/Cookies';
|
||||
import Notifications from 'components/Notifications';
|
||||
import Portal from 'components/Portal';
|
||||
import { showPreferences, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import { showPreferences } from 'providers/ReduxStore/slices/app';
|
||||
import { openConsole } from 'providers/ReduxStore/slices/logs';
|
||||
import { useApp } from 'providers/App';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -16,7 +15,6 @@ const StatusBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const preferencesOpen = useSelector((state) => state.app.showPreferences);
|
||||
const logs = useSelector((state) => state.logs.logs);
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const [cookiesOpen, setCookiesOpen] = useState(false);
|
||||
const { version } = useApp();
|
||||
|
||||
@@ -61,16 +59,6 @@ const StatusBar = () => {
|
||||
<div className="status-bar">
|
||||
<div className="status-bar-section">
|
||||
<div className="status-bar-group">
|
||||
<ToolHint text="Toggle Sidebar" toolhintId="Toggle Sidebar" place="top-start" offset={10}>
|
||||
<button
|
||||
className="status-bar-button"
|
||||
aria-label="Toggle Sidebar"
|
||||
onClick={() => dispatch(toggleSidebarCollapse())}
|
||||
>
|
||||
<IconSidebarToggle collapsed={sidebarCollapsed} size={16} strokeWidth={1.5} aria-hidden="true" />
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
<ToolHint text="Preferences" toolhintId="Preferences" place="top-start" offset={10}>
|
||||
<button
|
||||
className="status-bar-button"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import { IconBrandGithub, IconPlus, IconDownload, IconFolders, IconSpeakerphone, IconBook } from '@tabler/icons';
|
||||
|
||||
import Bruno from 'components/Bruno';
|
||||
@@ -15,19 +14,13 @@ import StyledWrapper from './StyledWrapper';
|
||||
const Welcome = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection())
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
toast.error(t('WELCOME.COLLECTION_OPEN_ERROR'));
|
||||
});
|
||||
dispatch(openCollection()).catch((err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR')));
|
||||
};
|
||||
|
||||
const handleImportCollection = ({ collection }) => {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
||||
import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import { getKeyBindingsForActionAllOS } from './keyMappings';
|
||||
|
||||
export const HotkeysContext = React.createContext();
|
||||
@@ -225,18 +224,6 @@ export const HotkeysProvider = (props) => {
|
||||
};
|
||||
}, [activeTabUid, tabs, collections, dispatch]);
|
||||
|
||||
// Collapse sidebar (ctrl/cmd + \)
|
||||
useEffect(() => {
|
||||
Mousetrap.bind([...getKeyBindingsForActionAllOS('collapseSidebar')], (e) => {
|
||||
dispatch(toggleSidebarCollapse());
|
||||
return false;
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind([...getKeyBindingsForActionAllOS('collapseSidebar')]);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const currentCollection = getCurrentCollection();
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,8 +20,7 @@ const KeyMapping = {
|
||||
windows: 'ctrl+pagedown',
|
||||
name: 'Switch to Next Tab'
|
||||
},
|
||||
closeAllTabs: { mac: 'command+shift+w', windows: 'ctrl+shift+w', name: 'Close All Tabs' },
|
||||
collapseSidebar: { mac: 'command+\\', windows: 'ctrl+\\', name: 'Collapse Sidebar' }
|
||||
closeAllTabs: { mac: 'command+shift+w', windows: 'ctrl+shift+w', name: 'Close All Tabs' }
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,6 @@ const initialState = {
|
||||
isDragging: false,
|
||||
idbConnectionReady: false,
|
||||
leftSidebarWidth: 222,
|
||||
sidebarCollapsed: false,
|
||||
screenWidth: 500,
|
||||
showHomePage: false,
|
||||
showPreferences: false,
|
||||
@@ -90,9 +89,6 @@ export const appSlice = createSlice({
|
||||
...state.generateCode,
|
||||
...action.payload
|
||||
};
|
||||
},
|
||||
toggleSidebarCollapse: (state) => {
|
||||
state.sidebarCollapsed = !state.sidebarCollapsed;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -112,8 +108,7 @@ export const {
|
||||
removeTaskFromQueue,
|
||||
removeAllTasksFromQueue,
|
||||
updateSystemProxyEnvVariables,
|
||||
updateGenerateCode,
|
||||
toggleSidebarCollapse
|
||||
updateGenerateCode
|
||||
} = appSlice.actions;
|
||||
|
||||
export const savePreferences = (preferences) => (dispatch, getState) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
import trim from 'lodash/trim';
|
||||
import path from 'utils/common/path';
|
||||
import { insertTaskIntoQueue, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';
|
||||
import toast from 'react-hot-toast';
|
||||
import {
|
||||
findCollectionByUid,
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
} from './index';
|
||||
|
||||
import { each } from 'lodash';
|
||||
import { closeAllCollectionTabs, updateResponsePaneScrollPosition } from 'providers/ReduxStore/slices/tabs';
|
||||
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { resolveRequestFilename } from 'utils/common/platform';
|
||||
import { parsePathParams, splitOnFirst } from 'utils/url/index';
|
||||
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
|
||||
@@ -260,13 +260,6 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
const requestUid = uuid();
|
||||
itemCopy.requestUid = requestUid;
|
||||
|
||||
await dispatch(
|
||||
updateResponsePaneScrollPosition({
|
||||
uid: state.tabs.activeTabUid,
|
||||
scrollY: 0
|
||||
})
|
||||
);
|
||||
|
||||
await dispatch(
|
||||
initRunRequestEvent({
|
||||
requestUid,
|
||||
@@ -1462,14 +1455,7 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
|
||||
collectionSchema
|
||||
.validate(collection)
|
||||
.then(() => dispatch(_createCollection({ ...collection, securityConfig })))
|
||||
.then(() => {
|
||||
// Expand sidebar if it's collapsed after collection is successfully opened
|
||||
const state = getState();
|
||||
if (state.app.sidebarCollapsed) {
|
||||
dispatch(toggleSidebarCollapse());
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
});
|
||||
@@ -1669,33 +1655,27 @@ export const clearOauth2Cache = (payload) => async (dispatch, getState) => {
|
||||
};
|
||||
|
||||
// todo: could be removed
|
||||
export const loadRequestViaWorker =
|
||||
({ collectionUid, pathname }) =>
|
||||
(dispatch, getState) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:load-request-via-worker', { collectionUid, pathname }).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
export const loadRequestViaWorker = ({ collectionUid, pathname }) => (dispatch, getState) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:load-request-via-worker', { collectionUid, pathname }).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
// todo: could be removed
|
||||
export const loadRequest =
|
||||
({ collectionUid, pathname }) =>
|
||||
(dispatch, getState) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:load-request', { collectionUid, pathname }).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
export const loadRequest = ({ collectionUid, pathname }) => (dispatch, getState) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:load-request', { collectionUid, pathname }).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const loadLargeRequest =
|
||||
({ collectionUid, pathname }) =>
|
||||
(dispatch, getState) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:load-large-request', { collectionUid, pathname }).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
export const loadLargeRequest = ({ collectionUid, pathname }) => (dispatch, getState) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:load-large-request', { collectionUid, pathname }).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const mountCollection =
|
||||
({ collectionUid, collectionPathname, brunoConfig }) =>
|
||||
@@ -1719,17 +1699,16 @@ export const showInFolder = (collectionPath) => () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const updateRunnerConfiguration =
|
||||
(collectionUid, selectedRequestItems, requestItemsOrder, delay) => (dispatch) => {
|
||||
dispatch(
|
||||
_updateRunnerConfiguration({
|
||||
collectionUid,
|
||||
selectedRequestItems,
|
||||
requestItemsOrder,
|
||||
delay
|
||||
})
|
||||
);
|
||||
};
|
||||
export const updateRunnerConfiguration = (collectionUid, selectedRequestItems, requestItemsOrder, delay) => (dispatch) => {
|
||||
dispatch(
|
||||
_updateRunnerConfiguration({
|
||||
collectionUid,
|
||||
selectedRequestItems,
|
||||
requestItemsOrder,
|
||||
delay
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const updateActiveConnectionsInStore = (activeConnectionIds) => (dispatch, getState) => {
|
||||
dispatch(updateActiveConnections(activeConnectionIds));
|
||||
|
||||
@@ -62,10 +62,10 @@ export const tabsSlice = createSlice({
|
||||
? preview
|
||||
: !nonReplaceableTabTypes.includes(type),
|
||||
...(uid ? { folderUid: uid } : {})
|
||||
};
|
||||
}
|
||||
|
||||
state.activeTabUid = uid;
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
state.tabs.push({
|
||||
@@ -74,7 +74,6 @@ export const tabsSlice = createSlice({
|
||||
requestPaneWidth: null,
|
||||
requestPaneTab: requestPaneTab || defaultRequestPaneTab,
|
||||
responsePaneTab: 'response',
|
||||
responsePaneScrollPosition: null,
|
||||
type: type || 'request',
|
||||
...(uid ? { folderUid: uid } : {}),
|
||||
preview: preview !== undefined
|
||||
@@ -127,13 +126,6 @@ export const tabsSlice = createSlice({
|
||||
tab.responsePaneTab = action.payload.responsePaneTab;
|
||||
}
|
||||
},
|
||||
updateResponsePaneScrollPosition: (state, action) => {
|
||||
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
|
||||
|
||||
if (tab) {
|
||||
tab.responsePaneScrollPosition = action.payload.scrollY;
|
||||
}
|
||||
},
|
||||
closeTabs: (state, action) => {
|
||||
const activeTab = find(state.tabs, (t) => t.uid === state.activeTabUid);
|
||||
const tabUids = action.payload.tabUids || [];
|
||||
@@ -175,8 +167,8 @@ export const tabsSlice = createSlice({
|
||||
const tab = find(state.tabs, (t) => t.uid === uid);
|
||||
if (tab) {
|
||||
tab.preview = false;
|
||||
} else {
|
||||
console.error('Tab not found!');
|
||||
} else{
|
||||
console.error("Tab not found!")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +181,6 @@ export const {
|
||||
updateRequestPaneTabWidth,
|
||||
updateRequestPaneTab,
|
||||
updateResponsePaneTab,
|
||||
updateResponsePaneScrollPosition,
|
||||
closeTabs,
|
||||
closeAllCollectionTabs,
|
||||
makeTabPermanent
|
||||
|
||||
@@ -131,10 +131,6 @@
|
||||
--px-12: 2px !important;
|
||||
}
|
||||
|
||||
.CodeMirror-hints {
|
||||
z-index: 20 !important;
|
||||
}
|
||||
|
||||
.graphiql-container {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,43 @@
|
||||
import fileDialog from 'file-dialog';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, transformItemsInCollection, updateUidsInCollection, hydrateSeqInCollection } from './common';
|
||||
|
||||
|
||||
export const processBrunoCollection = async (jsonData) => {
|
||||
try {
|
||||
let collection = hydrateSeqInCollection(jsonData);
|
||||
collection = updateUidsInCollection(collection);
|
||||
collection = transformItemsInCollection(collection);
|
||||
await validateSchema(collection);
|
||||
return collection;
|
||||
} catch (err) {
|
||||
console.error('Error processing Bruno collection:', err);
|
||||
throw new BrunoError('Import collection failed');
|
||||
}
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => resolve(e.target.result);
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
const parseJsonCollection = (str) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
let parsed = JSON.parse(str);
|
||||
return resolve(parsed);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
reject(new BrunoError('Unable to parse the collection json file'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parseJsonCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(updateUidsInCollection)
|
||||
.then(transformItemsInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => resolve({ collection }))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
reject(new BrunoError('Import collection failed'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default importCollection;
|
||||
|
||||
@@ -1,26 +1,43 @@
|
||||
import jsyaml from 'js-yaml';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { insomniaToBruno } from '@usebruno/converters';
|
||||
|
||||
|
||||
export const convertInsomniaToBruno = (data) => {
|
||||
try {
|
||||
return insomniaToBruno(data);
|
||||
} catch (err) {
|
||||
console.error('Error converting Insomnia to Bruno:', err);
|
||||
throw new BrunoError('Import collection failed: ' + err.message);
|
||||
}
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => {
|
||||
try {
|
||||
// try to load JSON
|
||||
const parsedData = JSON.parse(e.target.result);
|
||||
resolve(parsedData);
|
||||
} catch (jsonError) {
|
||||
// not a valid JSOn, try yaml
|
||||
try {
|
||||
const parsedData = jsyaml.load(e.target.result, { schema: jsyaml.CORE_SCHEMA });
|
||||
resolve(parsedData);
|
||||
} catch (yamlError) {
|
||||
console.error('Error parsing the file :', jsonError, yamlError);
|
||||
reject(new BrunoError('Import collection failed'));
|
||||
}
|
||||
}
|
||||
};
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
export const isInsomniaCollection = (data) => {
|
||||
// Check for Insomnia v5 collection format – collection array must be present
|
||||
if (typeof data.type === 'string' && data.type.startsWith('collection.insomnia.rest/5')) {
|
||||
return Array.isArray(data.collection);
|
||||
}
|
||||
|
||||
// Check for Insomnia v4 export format – must have __export_format and resources array
|
||||
if (data._type === 'export') {
|
||||
return Array.isArray(data.resources) && typeof data.__export_format === 'number';
|
||||
}
|
||||
|
||||
return false;
|
||||
const importCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' })
|
||||
.then(readFile)
|
||||
.then((collection) => insomniaToBruno(collection))
|
||||
.then((collection) => resolve({ collection }))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
reject(new BrunoError('Import collection failed: ' + err.message));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default importCollection;
|
||||
|
||||
@@ -1,27 +1,43 @@
|
||||
import jsyaml from 'js-yaml';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { openApiToBruno } from '@usebruno/converters';
|
||||
|
||||
export const convertOpenapiToBruno = (data) => {
|
||||
try {
|
||||
return openApiToBruno(data);
|
||||
} catch (err) {
|
||||
console.error('Error converting OpenAPI to Bruno:', err);
|
||||
throw new BrunoError('Import collection failed: ' + err.message);
|
||||
}
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => {
|
||||
try {
|
||||
// try to load JSON
|
||||
const parsedData = JSON.parse(e.target.result);
|
||||
resolve(parsedData);
|
||||
} catch (jsonError) {
|
||||
// not a valid JSOn, try yaml
|
||||
try {
|
||||
const parsedData = jsyaml.load(e.target.result);
|
||||
resolve(parsedData);
|
||||
} catch (yamlError) {
|
||||
console.error('Error parsing the file :', jsonError, yamlError);
|
||||
reject(new BrunoError('Import collection failed'));
|
||||
}
|
||||
}
|
||||
};
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
export const isOpenApiSpec = (data) => {
|
||||
if (typeof data.info !== 'object' || data.info === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof data.openapi === 'string' && data.openapi.trim().length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof data.swagger === 'string' && data.swagger.trim().length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
const importCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' })
|
||||
.then(readFile)
|
||||
.then((collection) => openApiToBruno(collection))
|
||||
.then((collection) => resolve({ collection }))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
reject(new BrunoError('Import collection failed: ' + err.message));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default importCollection;
|
||||
|
||||
@@ -22,20 +22,4 @@ const postmanToBruno = (collection) => {
|
||||
});
|
||||
};
|
||||
|
||||
const isPostmanCollection = (data) => {
|
||||
const info = data.info;
|
||||
if (!info || typeof info !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const schema = info.schema;
|
||||
// Accept schemas hosted at schema.getpostman.com or schema.postman.com
|
||||
const schemaRegex = /^https:\/\/schema\.(?:getpostman|postman)\.com\//;
|
||||
if (typeof schema === 'string' && schemaRegex.test(schema)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export { postmanToBruno, readFile, isPostmanCollection };
|
||||
export { postmanToBruno, readFile };
|
||||
|
||||
@@ -111,7 +111,7 @@ function makeAxiosInstance({ requestMaxRedirects = 5, disableCookies } = {}) {
|
||||
}
|
||||
|
||||
if (!disableCookies){
|
||||
saveCookies(error.config.url, error.response.headers);
|
||||
saveCookies(redirectUrl, error.response.headers);
|
||||
}
|
||||
|
||||
const requestConfig = createRedirectConfig(error, redirectUrl);
|
||||
|
||||
@@ -1,38 +1,18 @@
|
||||
import interpolate from './index';
|
||||
import moment from 'moment';
|
||||
|
||||
const BRUNO_BIRTH_DATE = new Date('2019-08-08');
|
||||
|
||||
const calculateAgeFromBirthDate = (birthDate = BRUNO_BIRTH_DATE) => {
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - birthDate.getFullYear();
|
||||
|
||||
const hasBirthdayPassedThisYear =
|
||||
today.getMonth() > birthDate.getMonth() ||
|
||||
(today.getMonth() === birthDate.getMonth() && today.getDate() >= birthDate.getDate());
|
||||
|
||||
if (!hasBirthdayPassedThisYear) {
|
||||
age--;
|
||||
}
|
||||
|
||||
return age;
|
||||
};
|
||||
|
||||
const BRUNO_AGE = calculateAgeFromBirthDate(BRUNO_BIRTH_DATE);
|
||||
|
||||
describe('interpolate', () => {
|
||||
it('should replace placeholders with values from the object', () => {
|
||||
const inputString = 'Hello, my name is {{user.name}} and I am {{user.age}} years old';
|
||||
const inputObject = {
|
||||
'user.name': 'Bruno',
|
||||
user: {
|
||||
age: BRUNO_AGE
|
||||
age: 4
|
||||
}
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe(`Hello, my name is Bruno and I am ${BRUNO_AGE} years old`);
|
||||
expect(result).toBe('Hello, my name is Bruno and I am 4 years old');
|
||||
});
|
||||
|
||||
it('should handle missing values by leaving the placeholders unchanged using {{}} as delimiters', () => {
|
||||
@@ -52,7 +32,7 @@ describe('interpolate', () => {
|
||||
const inputObject = {
|
||||
user: {
|
||||
full_name: 'Bruno',
|
||||
age: BRUNO_AGE,
|
||||
age: 4,
|
||||
'fav-food': ['egg', 'meat'],
|
||||
'want.attention': true
|
||||
}
|
||||
@@ -65,7 +45,7 @@ describe('interpolate', () => {
|
||||
`;
|
||||
const expectedStr = `
|
||||
Hi, I am Bruno,
|
||||
I am ${BRUNO_AGE} years old.
|
||||
I am 4 years old.
|
||||
My favorite food is egg and meat.
|
||||
I like attention: true
|
||||
`;
|
||||
@@ -78,13 +58,13 @@ describe('interpolate', () => {
|
||||
const inputObject = {
|
||||
'user.name': 'Bruno',
|
||||
user: {
|
||||
age: BRUNO_AGE
|
||||
age: 4
|
||||
}
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe(`Hello, my name is {{ user.name }} and I am ${BRUNO_AGE} years old`);
|
||||
expect(result).toBe('Hello, my name is {{ user.name }} and I am 4 years old');
|
||||
});
|
||||
|
||||
test('should give precedence to the last key in case of duplicates (not at the top level)', () => {
|
||||
@@ -94,14 +74,14 @@ describe('interpolate', () => {
|
||||
'user.name': 'Bruno',
|
||||
user: {
|
||||
name: 'Not _Bruno_',
|
||||
age: BRUNO_AGE
|
||||
age: 4
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe(`Hello, my name is Bruno and Not _Bruno_ I am ${BRUNO_AGE} years old`);
|
||||
expect(result).toBe('Hello, my name is Bruno and Not _Bruno_ I am 4 years old');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,13 +179,13 @@ describe('interpolate - recursive', () => {
|
||||
'user.message': 'Hello, my name is {{user.name}} and I am {{user.age}} years old',
|
||||
'user.name': 'Bruno',
|
||||
user: {
|
||||
age: BRUNO_AGE
|
||||
age: 4
|
||||
}
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe(`Hello, my name is Bruno and I am ${BRUNO_AGE} years old`);
|
||||
expect(result).toBe('Hello, my name is Bruno and I am 4 years old');
|
||||
});
|
||||
|
||||
it('should replace placeholders with 2 level of recursion with values from the object', () => {
|
||||
@@ -215,13 +195,13 @@ describe('interpolate - recursive', () => {
|
||||
'user.name': 'Bruno {{user.lastName}}',
|
||||
'user.lastName': 'Dog',
|
||||
user: {
|
||||
age: BRUNO_AGE
|
||||
age: 4
|
||||
}
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe(`Hello, my name is Bruno Dog and I am ${BRUNO_AGE} years old`);
|
||||
expect(result).toBe('Hello, my name is Bruno Dog and I am 4 years old');
|
||||
});
|
||||
|
||||
it('should replace placeholders with 3 level of recursion with values from the object', () => {
|
||||
@@ -232,13 +212,13 @@ describe('interpolate - recursive', () => {
|
||||
'user.name': 'Bruno {{user.lastName}}',
|
||||
'user.lastName': 'Dog',
|
||||
user: {
|
||||
age: BRUNO_AGE
|
||||
age: 4
|
||||
}
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe(`Hello, my name is Bruno Dog and I am ${BRUNO_AGE} years old`);
|
||||
expect(result).toBe('Hello, my name is Bruno Dog and I am 4 years old');
|
||||
});
|
||||
|
||||
it('should handle missing values with 1 level of recursion by leaving the placeholders unchanged using {{}} as delimiters', () => {
|
||||
@@ -246,13 +226,13 @@ describe('interpolate - recursive', () => {
|
||||
const inputObject = {
|
||||
'user.message': 'Hello, my name is {{user.name}} and I am {{user.age}} years old',
|
||||
user: {
|
||||
age: BRUNO_AGE
|
||||
age: 4
|
||||
}
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe(`Hello, my name is {{user.name}} and I am ${BRUNO_AGE} years old`);
|
||||
expect(result).toBe('Hello, my name is {{user.name}} and I am 4 years old');
|
||||
});
|
||||
|
||||
it('should handle all valid keys with 1 level of recursion', () => {
|
||||
@@ -266,7 +246,7 @@ describe('interpolate - recursive', () => {
|
||||
user: {
|
||||
message,
|
||||
full_name: 'Bruno',
|
||||
age: BRUNO_AGE,
|
||||
age: 4,
|
||||
'fav-food': ['egg', 'meat'],
|
||||
'want.attention': true
|
||||
}
|
||||
@@ -275,7 +255,7 @@ describe('interpolate - recursive', () => {
|
||||
const inputStr = '{{user.message}}';
|
||||
const expectedStr = `
|
||||
Hi, I am Bruno,
|
||||
I am ${BRUNO_AGE} years old.
|
||||
I am 4 years old.
|
||||
My favorite food is egg and meat.
|
||||
I like attention: true
|
||||
`;
|
||||
@@ -381,32 +361,32 @@ describe('interpolate - object handling', () => {
|
||||
it('should stringify simple objects', () => {
|
||||
const inputString = 'User: {{user}}';
|
||||
const inputObject = {
|
||||
'user': { name: 'Bruno', age: BRUNO_AGE }
|
||||
'user': { name: 'Bruno', age: 4 }
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe(`User: {"name":"Bruno","age":${BRUNO_AGE}}`);
|
||||
expect(result).toBe('User: {"name":"Bruno","age":4}');
|
||||
});
|
||||
|
||||
it('should stringify simple objects (dot notation)', () => {
|
||||
const inputString = 'User: {{user.data}}';
|
||||
const inputObject = {
|
||||
'user.data': { name: 'Bruno', age: BRUNO_AGE }
|
||||
'user.data': { name: 'Bruno', age: 4 }
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe(`User: {"name":"Bruno","age":${BRUNO_AGE}}`);
|
||||
expect(result).toBe('User: {"name":"Bruno","age":4}');
|
||||
});
|
||||
|
||||
it('should stringify nested objects', () => {
|
||||
const inputString = 'User: {{user}}';
|
||||
const inputObject = {
|
||||
'user': {
|
||||
name: 'Bruno',
|
||||
age: BRUNO_AGE,
|
||||
preferences: {
|
||||
'user': {
|
||||
name: 'Bruno',
|
||||
age: 4,
|
||||
preferences: {
|
||||
food: ['egg', 'meat'],
|
||||
toys: { favorite: 'ball' }
|
||||
}
|
||||
@@ -415,7 +395,7 @@ describe('interpolate - object handling', () => {
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe(`User: {"name":"Bruno","age":${BRUNO_AGE},"preferences":{"food":["egg","meat"],"toys":{"favorite":"ball"}}}`);
|
||||
expect(result).toBe('User: {"name":"Bruno","age":4,"preferences":{"food":["egg","meat"],"toys":{"favorite":"ball"}}}');
|
||||
});
|
||||
|
||||
it('should stringify arrays', () => {
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
* Ex: interpolate('Hello, my name is ${user.name} and I am ${user.age} years old', {
|
||||
* "user.name": "Bruno",
|
||||
* "user": {
|
||||
* "age": 6
|
||||
* "age": 4
|
||||
* }
|
||||
* });
|
||||
* Output: Hello, my name is Bruno and I am 6 years old
|
||||
* Output: Hello, my name is Bruno and I am 4 years old
|
||||
*/
|
||||
|
||||
import { mockDataFunctions } from '../utils/faker-functions';
|
||||
|
||||
@@ -31,7 +31,7 @@ const brunoEnvironment = postmanToBrunoEnvironment(postmanEnvironment);
|
||||
### Convert Insomnia collection to Bruno collection
|
||||
|
||||
```javascript
|
||||
const { insomniaToBruno } = require('@usebruno/converters');
|
||||
import { insomniaToBruno } from '@usebruno/converters';
|
||||
|
||||
const brunoCollection = insomniaToBruno(insomniaCollection);
|
||||
```
|
||||
@@ -39,7 +39,7 @@ const brunoCollection = insomniaToBruno(insomniaCollection);
|
||||
### Convert OpenAPI specification to Bruno collection
|
||||
|
||||
```javascript
|
||||
const { openApiToBruno } = require('@usebruno/converters');
|
||||
import { openApiToBruno } from '@usebruno/converters';
|
||||
|
||||
const brunoCollection = openApiToBruno(openApiSpecification);
|
||||
```
|
||||
@@ -75,4 +75,4 @@ const outputFilePath = path.resolve(__dirname, 'bruno-collection.json');
|
||||
|
||||
convertPostmanToBruno(inputFilePath, outputFilePath);
|
||||
|
||||
```
|
||||
```
|
||||
@@ -60,9 +60,7 @@ const transformOpenapiRequestItem = (request) => {
|
||||
mode: 'inherit',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
digest: null,
|
||||
apikey: null,
|
||||
oauth2: null
|
||||
digest: null
|
||||
},
|
||||
headers: [],
|
||||
params: [],
|
||||
@@ -110,16 +108,13 @@ const transformOpenapiRequestItem = (request) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Handle explicit no-auth case where security: [] on the operation
|
||||
if (Array.isArray(_operationObject.security) && _operationObject.security.length === 0) {
|
||||
brunoRequestItem.request.auth.mode = 'inherit';
|
||||
return brunoRequestItem;
|
||||
}
|
||||
|
||||
let auth = null;
|
||||
let auth;
|
||||
// allow operation override
|
||||
if (_operationObject.security && _operationObject.security.length > 0) {
|
||||
const schemeName = Object.keys(_operationObject.security[0])[0];
|
||||
let schemeName = Object.keys(_operationObject.security[0])[0];
|
||||
auth = request.global.security.getScheme(schemeName);
|
||||
} else if (request.global.security.supported.length > 0) {
|
||||
auth = request.global.security.supported[0];
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
@@ -134,87 +129,14 @@ const transformOpenapiRequestItem = (request) => {
|
||||
brunoRequestItem.request.auth.bearer = {
|
||||
token: '{{token}}'
|
||||
};
|
||||
} else if (auth.type === 'http' && auth.scheme === 'digest') {
|
||||
brunoRequestItem.request.auth.mode = 'digest';
|
||||
brunoRequestItem.request.auth.digest = {
|
||||
username: '{{username}}',
|
||||
password: '{{password}}'
|
||||
};
|
||||
} else if (auth.type === 'apiKey') {
|
||||
const apikeyConfig = {
|
||||
key: auth.name,
|
||||
} else if (auth.type === 'apiKey' && auth.in === 'header') {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: auth.name,
|
||||
value: '{{apiKey}}',
|
||||
placement: auth.in === 'query' ? 'queryparams' : 'header'
|
||||
};
|
||||
brunoRequestItem.request.auth.mode = 'apikey';
|
||||
brunoRequestItem.request.auth.apikey = apikeyConfig;
|
||||
|
||||
if (auth.in === 'header' || auth.in === 'cookie') {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: auth.name,
|
||||
value: '{{apiKey}}',
|
||||
description: auth.description || '',
|
||||
enabled: true
|
||||
});
|
||||
} else if (auth.in === 'query') {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: auth.name,
|
||||
value: '{{apiKey}}',
|
||||
description: auth.description || '',
|
||||
enabled: true,
|
||||
type: 'query'
|
||||
});
|
||||
}
|
||||
} else if (auth.type === 'oauth2') {
|
||||
// Determine flow (grant type)
|
||||
let flows = auth.flows || {};
|
||||
let grantType = 'client_credentials';
|
||||
if (flows.authorizationCode) {
|
||||
grantType = 'authorization_code';
|
||||
} else if (flows.implicit) {
|
||||
grantType = 'implicit';
|
||||
} else if (flows.password) {
|
||||
grantType = 'password';
|
||||
} else if (flows.clientCredentials) {
|
||||
grantType = 'client_credentials';
|
||||
}
|
||||
|
||||
let flowConfig = {};
|
||||
switch (grantType) {
|
||||
case 'authorization_code':
|
||||
flowConfig = flows.authorizationCode || {};
|
||||
break;
|
||||
case 'implicit':
|
||||
flowConfig = flows.implicit || {};
|
||||
break;
|
||||
case 'password':
|
||||
flowConfig = flows.password || {};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
default:
|
||||
flowConfig = flows.clientCredentials || {};
|
||||
break;
|
||||
}
|
||||
|
||||
brunoRequestItem.request.auth.mode = 'oauth2';
|
||||
brunoRequestItem.request.auth.oauth2 = {
|
||||
grantType: grantType,
|
||||
authorizationUrl: flowConfig.authorizationUrl || '{{oauth_authorize_url}}',
|
||||
accessTokenUrl: flowConfig.tokenUrl || '{{oauth_token_url}}',
|
||||
refreshTokenUrl: flowConfig.refreshUrl || '{{oauth_refresh_url}}',
|
||||
callbackUrl: '{{oauth_callback_url}}',
|
||||
clientId: '{{oauth_client_id}}',
|
||||
clientSecret: '{{oauth_client_secret}}',
|
||||
scope: Array.isArray(flowConfig.scopes) ? flowConfig.scopes.join(' ') : Object.keys(flowConfig.scopes || {}).join(' '),
|
||||
state: '{{oauth_state}}',
|
||||
credentialsPlacement: 'header',
|
||||
tokenPlacement: 'header',
|
||||
tokenHeaderPrefix: 'Bearer',
|
||||
autoFetchToken: false,
|
||||
autoRefreshToken: true
|
||||
};
|
||||
description: 'Authentication header',
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,9 +425,7 @@ export const parseOpenApiCollection = (data) => {
|
||||
mode: 'inherit',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
digest: null,
|
||||
apikey: null,
|
||||
oauth2: null
|
||||
digest: null
|
||||
}
|
||||
},
|
||||
meta: {
|
||||
@@ -519,103 +439,6 @@ export const parseOpenApiCollection = (data) => {
|
||||
let ungroupedItems = ungroupedRequests.map(transformOpenapiRequestItem);
|
||||
let brunoCollectionItems = brunoFolders.concat(ungroupedItems);
|
||||
brunoCollection.items = brunoCollectionItems;
|
||||
|
||||
// Determine collection-level authentication based on global security requirements
|
||||
const buildCollectionAuth = (scheme) => {
|
||||
const authTemplate = {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
digest: null,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
};
|
||||
|
||||
if (!scheme) return authTemplate;
|
||||
|
||||
if (scheme.type === 'http' && scheme.scheme === 'basic') {
|
||||
return {
|
||||
...authTemplate,
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username: '{{username}}',
|
||||
password: '{{password}}'
|
||||
}
|
||||
};
|
||||
} else if (scheme.type === 'http' && scheme.scheme === 'bearer') {
|
||||
return {
|
||||
...authTemplate,
|
||||
mode: 'bearer',
|
||||
bearer: {
|
||||
token: '{{token}}'
|
||||
}
|
||||
};
|
||||
} else if (scheme.type === 'http' && scheme.scheme === 'digest') {
|
||||
return {
|
||||
...authTemplate,
|
||||
mode: 'digest',
|
||||
digest: {
|
||||
username: '{{username}}',
|
||||
password: '{{password}}'
|
||||
}
|
||||
};
|
||||
} else if (scheme.type === 'apiKey') {
|
||||
return {
|
||||
...authTemplate,
|
||||
mode: 'apikey',
|
||||
apikey: {
|
||||
key: scheme.name,
|
||||
value: '{{apiKey}}',
|
||||
placement: scheme.in === 'query' ? 'queryparams' : 'header'
|
||||
}
|
||||
};
|
||||
} else if (scheme.type === 'oauth2') {
|
||||
let flows = scheme.flows || {};
|
||||
let grantType = 'client_credentials';
|
||||
if (flows.authorizationCode) {
|
||||
grantType = 'authorization_code';
|
||||
} else if (flows.implicit) {
|
||||
grantType = 'implicit';
|
||||
} else if (flows.password) {
|
||||
grantType = 'password';
|
||||
}
|
||||
const flowConfig = grantType === 'authorization_code' ? flows.authorizationCode || {} : grantType === 'implicit' ? flows.implicit || {} : grantType === 'password' ? flows.password || {} : flows.clientCredentials || {};
|
||||
|
||||
return {
|
||||
...authTemplate,
|
||||
mode: 'oauth2',
|
||||
oauth2: {
|
||||
grantType,
|
||||
authorizationUrl: flowConfig.authorizationUrl || '{{oauth_authorize_url}}',
|
||||
accessTokenUrl: flowConfig.tokenUrl || '{{oauth_token_url}}',
|
||||
refreshTokenUrl: flowConfig.refreshUrl || '{{oauth_refresh_url}}',
|
||||
callbackUrl: '{{oauth_callback_url}}',
|
||||
clientId: '{{oauth_client_id}}',
|
||||
clientSecret: '{{oauth_client_secret}}',
|
||||
scope: Array.isArray(flowConfig.scopes) ? flowConfig.scopes.join(' ') : Object.keys(flowConfig.scopes || {}).join(' '),
|
||||
state: '{{oauth_state}}',
|
||||
credentialsPlacement: 'header',
|
||||
tokenPlacement: 'header',
|
||||
tokenHeaderPrefix: 'Bearer',
|
||||
autoFetchToken: false,
|
||||
autoRefreshToken: true
|
||||
}
|
||||
};
|
||||
}
|
||||
return authTemplate;
|
||||
};
|
||||
|
||||
let collectionAuth = buildCollectionAuth(securityConfig.supported[0]);
|
||||
|
||||
brunoCollection.root = {
|
||||
request: {
|
||||
auth: collectionAuth,
|
||||
},
|
||||
meta: {
|
||||
name: brunoCollection.name
|
||||
}
|
||||
};
|
||||
|
||||
return brunoCollection;
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
|
||||
@@ -350,9 +350,6 @@ function translateCode(code) {
|
||||
// Process all transformations in a single pass
|
||||
processTransformations(ast, transformedNodes);
|
||||
|
||||
// Handle legacy Postman global APIs
|
||||
handleLegacyGlobalAPIs(ast, transformedNodes, code);
|
||||
|
||||
// Handle special Postman syntax patterns
|
||||
handleTestsBracketNotation(ast);
|
||||
|
||||
@@ -790,102 +787,5 @@ function handleTestsBracketNotation(ast) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle legacy Postman global API transformations
|
||||
* This function processes legacy Postman globals like responseBody, responseHeaders, responseTime
|
||||
* while preserving user-defined variables with the same names
|
||||
*
|
||||
* @param {Object} ast - jscodeshift AST
|
||||
* @param {Set} transformedNodes - Set of already transformed nodes
|
||||
* @param {string} code - The original Postman script code
|
||||
*/
|
||||
function handleLegacyGlobalAPIs(ast, transformedNodes, code) {
|
||||
// regex check before the ast traversal
|
||||
const legacyGlobalRegex = /responseBody|responseHeaders|responseTime/;
|
||||
|
||||
if (!legacyGlobalRegex.test(code)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for variable declarations with legacy global names - track which ones have conflicts
|
||||
const conflictingNames = new Set();
|
||||
|
||||
// Check variable declarations
|
||||
ast.find(j.VariableDeclarator).forEach(path => {
|
||||
if (path.value.id.type === 'Identifier') {
|
||||
const varName = path.value.id.name;
|
||||
if (legacyGlobalRegex.test(varName)) {
|
||||
conflictingNames.add(varName);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle JSON.parse(responseBody) → res.getBody()
|
||||
// Only transform if responseBody doesn't have a user variable conflict
|
||||
if (!conflictingNames.has('responseBody')) {
|
||||
ast.find(j.CallExpression).forEach(path => {
|
||||
if (transformedNodes.has(path.node)) return;
|
||||
|
||||
const callExpr = path.value;
|
||||
if (callExpr.callee.type === 'MemberExpression' && callExpr.callee.object.name === 'JSON' && callExpr.callee.property.name === 'parse') {
|
||||
const args = callExpr.arguments;
|
||||
|
||||
// Check if the argument is 'responseBody'
|
||||
if (args.length > 0 && args[0].type === 'Identifier' && args[0].name === 'responseBody') {
|
||||
// Replace JSON.parse(responseBody) with res.getBody()
|
||||
j(path).replaceWith(j.identifier('res.getBody()'));
|
||||
transformedNodes.add(path.node);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle standalone legacy Postman global variables
|
||||
const legacyGlobals = [
|
||||
{ name: 'responseBody', replacement: 'res.getBody()' },
|
||||
{ name: 'responseHeaders', replacement: 'res.getHeaders()' },
|
||||
{ name: 'responseTime', replacement: 'res.getResponseTime()' }
|
||||
];
|
||||
|
||||
legacyGlobals.forEach(({ name, replacement }) => {
|
||||
// Skip transformation if this name has a user variable conflict
|
||||
if (conflictingNames.has(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ast.find(j.Identifier, { name }).forEach(path => {
|
||||
if (transformedNodes.has(path.node)) return;
|
||||
|
||||
// Only transform identifiers that are being used as values, not as variable names
|
||||
const parent = path.parent.value;
|
||||
|
||||
// Skip if this is part of a variable declaration (const responseBody = ...)
|
||||
if (parent.type === 'VariableDeclarator' && parent.id === path.node) {
|
||||
return; // Keep unchanged
|
||||
}
|
||||
|
||||
// Skip if this is part of an assignment (responseBody = ...)
|
||||
if (parent.type === 'AssignmentExpression' && parent.left === path.node) {
|
||||
return; // Keep unchanged
|
||||
}
|
||||
|
||||
// Skip if this is part of a function parameter
|
||||
if (parent.type === 'FunctionDeclaration' || parent.type === 'FunctionExpression') {
|
||||
return; // Keep unchanged
|
||||
}
|
||||
|
||||
// Skip if this is part of an object property
|
||||
if (parent.type === 'Property' && (parent.key === path.node || parent.value === path.node)) {
|
||||
return; // Keep unchanged
|
||||
}
|
||||
|
||||
// Transform all other references (including function call arguments)
|
||||
// This will transform console.log(responseBody) → console.log(res.getBody())
|
||||
j(path).replaceWith(j.identifier(replacement));
|
||||
transformedNodes.add(path.node);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export { getMemberExpressionString };
|
||||
export default translateCode;
|
||||
@@ -1,143 +0,0 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import openApiToBruno from '../../../src/openapi/openapi-to-bruno';
|
||||
|
||||
describe('openapi-to-bruno auth enhancements', () => {
|
||||
it('maps HTTP Digest scheme to digest auth on the request', () => {
|
||||
const spec = `
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Digest API
|
||||
version: '1.0'
|
||||
components:
|
||||
securitySchemes:
|
||||
DigestAuth:
|
||||
type: http
|
||||
scheme: digest
|
||||
paths:
|
||||
/secure:
|
||||
get:
|
||||
security:
|
||||
- DigestAuth: []
|
||||
responses:
|
||||
'200': { description: OK }
|
||||
servers:
|
||||
- url: https://example.com
|
||||
`;
|
||||
const collection = openApiToBruno(spec);
|
||||
const req = collection.items[0];
|
||||
expect(req.request.auth.mode).toBe('digest');
|
||||
expect(req.request.auth.digest).toEqual({ username: '{{username}}', password: '{{password}}' });
|
||||
});
|
||||
|
||||
it('maps apiKey in query and injects query param', () => {
|
||||
const spec = `
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Query API-Key
|
||||
version: '1.0'
|
||||
components:
|
||||
securitySchemes:
|
||||
ApiKeyQuery:
|
||||
type: apiKey
|
||||
in: query
|
||||
name: api_key
|
||||
paths:
|
||||
/search:
|
||||
get:
|
||||
security:
|
||||
- ApiKeyQuery: []
|
||||
parameters:
|
||||
- in: query
|
||||
name: q
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200': { description: OK }
|
||||
servers:
|
||||
- url: https://example.com
|
||||
`;
|
||||
const collection = openApiToBruno(spec);
|
||||
const req = collection.items[0];
|
||||
expect(req.request.auth.mode).toBe('apikey');
|
||||
expect(req.request.auth.apikey.placement).toBe('queryparams');
|
||||
const hasQueryParam = req.request.params.some(p => p.name === 'api_key' && p.type === 'query');
|
||||
expect(hasQueryParam).toBe(true);
|
||||
});
|
||||
|
||||
it('maps apiKey in cookie and treats it as a header', () => {
|
||||
const spec = `
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Cookie API-Key
|
||||
version: '1.0'
|
||||
components:
|
||||
securitySchemes:
|
||||
ApiKeyCookie:
|
||||
type: apiKey
|
||||
in: cookie
|
||||
name: DEMO_API_KEY
|
||||
paths:
|
||||
/favorites:
|
||||
get:
|
||||
security:
|
||||
- ApiKeyCookie: []
|
||||
responses:
|
||||
'200': { description: OK }
|
||||
servers:
|
||||
- url: https://example.com
|
||||
`;
|
||||
const { items: [req] } = openApiToBruno(spec);
|
||||
expect(req.request.auth.mode).toBe('apikey');
|
||||
expect(req.request.auth.apikey.placement).toBe('header');
|
||||
const apiKeyHeader = req.request.headers.find(h => h.name === 'DEMO_API_KEY');
|
||||
expect(apiKeyHeader).toBeDefined();
|
||||
expect(apiKeyHeader.value).toBe('{{apiKey}}');
|
||||
});
|
||||
|
||||
it('maps OAuth2 authorizationCode flow to oauth2 grantType authorization_code', () => {
|
||||
const spec = `
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: OAuth2 AuthCode
|
||||
version: '1.0'
|
||||
components:
|
||||
securitySchemes:
|
||||
OAuthAuthCode:
|
||||
type: oauth2
|
||||
flows:
|
||||
authorizationCode:
|
||||
authorizationUrl: https://auth.example.com/authorize
|
||||
tokenUrl: https://auth.example.com/token
|
||||
paths:
|
||||
/orders:
|
||||
get:
|
||||
security:
|
||||
- OAuthAuthCode: []
|
||||
responses:
|
||||
'200': { description: OK }
|
||||
servers:
|
||||
- url: https://example.com
|
||||
`;
|
||||
const { items: [req] } = openApiToBruno(spec);
|
||||
expect(req.request.auth.mode).toBe('oauth2');
|
||||
expect(req.request.auth.oauth2.grantType).toBe('authorization_code');
|
||||
});
|
||||
|
||||
it('sets auth mode to inherit when operation security is explicitly empty', () => {
|
||||
const spec = `
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Public Endpoint
|
||||
version: '1.0'
|
||||
paths:
|
||||
/public:
|
||||
get:
|
||||
security: []
|
||||
responses:
|
||||
'200': { description: OK }
|
||||
servers:
|
||||
- url: https://example.com
|
||||
`;
|
||||
const { items: [req] } = openApiToBruno(spec);
|
||||
expect(req.request.auth.mode).toBe('inherit');
|
||||
});
|
||||
});
|
||||
@@ -1,299 +0,0 @@
|
||||
import translateCode from '../../../../src/utils/jscode-shift-translator.js';
|
||||
|
||||
describe('Legacy Postman API Translation', () => {
|
||||
describe('handleLegacyGlobalAPIs - No Conflicts', () => {
|
||||
test('should translate responseBody when no user variables exist', () => {
|
||||
const input = `
|
||||
const data = JSON.parse(responseBody);
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
const expected = `
|
||||
const data = res.getBody();
|
||||
`;
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should translate responseHeaders when no user variables exist', () => {
|
||||
const input = `
|
||||
console.log(responseHeaders);
|
||||
const headers = responseHeaders;
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
|
||||
expect(result).toContain('res.getHeaders()');
|
||||
expect(result).not.toContain('responseHeaders');
|
||||
});
|
||||
|
||||
test('should translate responseTime when no user variables exist', () => {
|
||||
const input = `
|
||||
console.log(responseTime);
|
||||
const time = responseTime;
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
|
||||
expect(result).toContain('res.getResponseTime()');
|
||||
expect(result).not.toContain('responseTime');
|
||||
});
|
||||
|
||||
test('should translate JSON.parse(responseBody) when no user variables exist', () => {
|
||||
const input = `
|
||||
const data = JSON.parse(responseBody);
|
||||
console.log(data);
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
|
||||
expect(result).toContain('res.getBody()');
|
||||
expect(result).not.toContain('JSON.parse(responseBody)');
|
||||
expect(result).not.toContain('responseBody');
|
||||
});
|
||||
|
||||
test('should translate JSON.parse(responseBody) usage without assignment when no user variables exist', () => {
|
||||
const input = `
|
||||
console.log(JSON.parse(responseBody));
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
const expected = `
|
||||
console.log(res.getBody());
|
||||
`;
|
||||
|
||||
expect(result).toContain(expected);
|
||||
});
|
||||
|
||||
test('should translate all legacy APIs when no conflicts exist', () => {
|
||||
const input = `
|
||||
const data = JSON.parse(responseBody);
|
||||
const headers = responseHeaders;
|
||||
const time = responseTime;
|
||||
|
||||
console.log(data, headers, time);
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
|
||||
expect(result).toContain('res.getBody()');
|
||||
expect(result).toContain('res.getHeaders()');
|
||||
expect(result).toContain('res.getResponseTime()');
|
||||
expect(result).not.toContain('responseBody');
|
||||
expect(result).not.toContain('responseHeaders');
|
||||
expect(result).not.toContain('responseTime');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleLegacyGlobalAPIs - With Conflicts', () => {
|
||||
test('should NOT translate responseBody when user variable exists', () => {
|
||||
const input = `
|
||||
const responseBody = pm.response.json();
|
||||
console.log(responseBody);
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
const expected = `
|
||||
const responseBody = res.getBody();
|
||||
console.log(responseBody);
|
||||
`;
|
||||
|
||||
// pm.response.json() should be transformed to res.getBody() (Postman API transformation)
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should NOT translate responseHeaders when user variable exists', () => {
|
||||
const input = `
|
||||
const responseHeaders = pm.response.headers;
|
||||
console.log(responseHeaders);
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
const expected = `
|
||||
const responseHeaders = res.getHeaders();
|
||||
console.log(responseHeaders);
|
||||
`;
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should NOT translate responseTime when user variable exists', () => {
|
||||
const input = `
|
||||
const responseTime = pm.response.responseTime;
|
||||
console.log(responseTime);
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
const expected = `
|
||||
const responseTime = res.getResponseTime();
|
||||
console.log(responseTime);
|
||||
`;
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should NOT translate JSON.parse(responseBody) when user variable exists', () => {
|
||||
const input = `
|
||||
const responseBody = pm.response.json();
|
||||
const data = JSON.parse(responseBody);
|
||||
console.log(data);
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
const expected = `
|
||||
const responseBody = res.getBody();
|
||||
const data = JSON.parse(responseBody);
|
||||
console.log(data);
|
||||
`;
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleLegacyGlobalAPIs - Partial Conflicts', () => {
|
||||
test('should translate non-conflicting APIs when some conflicts exist', () => {
|
||||
const input = `
|
||||
const responseBody = pm.response.json();
|
||||
console.log(responseBody);
|
||||
console.log(responseHeaders);
|
||||
console.log(responseTime);
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
const expected = `
|
||||
const responseBody = res.getBody();
|
||||
console.log(responseBody);
|
||||
console.log(res.getHeaders());
|
||||
console.log(res.getResponseTime());
|
||||
`;
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
||||
});
|
||||
|
||||
test('should translate JSON.parse(responseBody) only when no conflict exists', () => {
|
||||
const input = `
|
||||
const responseHeaders = pm.response.headers;
|
||||
const data = JSON.parse(responseBody);
|
||||
console.log(responseHeaders);
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
const expected = `
|
||||
const responseHeaders = res.getHeaders();
|
||||
const data = res.getBody();
|
||||
console.log(responseHeaders);
|
||||
`;
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleLegacyGlobalAPIs - Edge Cases', () => {
|
||||
test.skip('should handle function parameters with legacy names', () => {
|
||||
const input = `
|
||||
function test(responseBody) {
|
||||
console.log(responseBody);
|
||||
console.log(responseHeaders);
|
||||
}
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
const expected = `
|
||||
function test(responseBody) {
|
||||
console.log(responseBody);
|
||||
console.log(res.getHeaders());
|
||||
}
|
||||
`;
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should handle object properties with legacy names', () => {
|
||||
const input = `
|
||||
const config = {
|
||||
responseBody: 'custom',
|
||||
responseHeaders: 'custom'
|
||||
};
|
||||
console.log(responseTime);
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
|
||||
const expected = `
|
||||
const config = {
|
||||
responseBody: 'custom',
|
||||
responseHeaders: 'custom'
|
||||
};
|
||||
console.log(res.getResponseTime());
|
||||
`;
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should handle assignments with legacy names', () => {
|
||||
const input = `
|
||||
responseBody = 'new value';
|
||||
responseHeaders = 'new headers';
|
||||
console.log(responseTime);
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
|
||||
const expected = `
|
||||
responseBody = 'new value';
|
||||
responseHeaders = 'new headers';
|
||||
console.log(res.getResponseTime());
|
||||
`;
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should handle mixed usage patterns', () => {
|
||||
const input = `
|
||||
const responseBody = pm.response.json();
|
||||
const data = JSON.parse(responseBody);
|
||||
console.log(responseHeaders);
|
||||
console.log(responseTime);
|
||||
|
||||
function test(data) {
|
||||
console.log(responseBody);
|
||||
console.log(responseHeaders);
|
||||
}
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
|
||||
const expected = `
|
||||
const responseBody = res.getBody();
|
||||
const data = JSON.parse(responseBody);
|
||||
console.log(res.getHeaders());
|
||||
console.log(res.getResponseTime());
|
||||
|
||||
function test(data) {
|
||||
console.log(responseBody);
|
||||
console.log(res.getHeaders());
|
||||
}
|
||||
`;
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleLegacyGlobalAPIs - No Legacy APIs', () => {
|
||||
test('should not modify code when no legacy APIs are present', () => {
|
||||
const input = `
|
||||
const data = { name: 'test' };
|
||||
console.log(data.name);
|
||||
`;
|
||||
|
||||
const result = translateCode(input);
|
||||
const expected = `
|
||||
const data = { name: 'test' };
|
||||
console.log(data.name);
|
||||
`;
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -174,7 +174,13 @@ app.on('ready', async () => {
|
||||
mainWindow.webContents.setZoomLevel(mainWindow.webContents.getZoomLevel() + 1);
|
||||
});
|
||||
|
||||
globalShortcut.register('CommandOrControl+M', () => {
|
||||
mainWindow.minimize();
|
||||
});
|
||||
|
||||
globalShortcut.register('CommandOrControl+H', () => {
|
||||
mainWindow.minimize();
|
||||
});
|
||||
|
||||
mainWindow.webContents.on('did-finish-load', async () => {
|
||||
let ogSend = mainWindow.webContents.send;
|
||||
|
||||
@@ -297,7 +297,7 @@ function makeAxiosInstance({
|
||||
}
|
||||
|
||||
if (preferencesUtil.shouldStoreCookies()) {
|
||||
saveCookies(error.config.url, error.response.headers);
|
||||
saveCookies(redirectUrl, error.response.headers);
|
||||
}
|
||||
|
||||
// Create a new request config for the redirect
|
||||
|
||||
@@ -56,13 +56,7 @@ const grammar = ohm.grammar(`Bru {
|
||||
// Dictionary Blocks
|
||||
dictionary = st* "{" pairlist? tagend
|
||||
pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
|
||||
pair = st* (quoted_key | key) st* ":" st* value st*
|
||||
disable_char = "~"
|
||||
quote_char = "\\""
|
||||
esc_char = "\\\\"
|
||||
esc_quote_char = esc_char quote_char
|
||||
quoted_key_char = ~(quote_char | esc_quote_char | nl) any
|
||||
quoted_key = disable_char? quote_char (esc_quote_char | quoted_key_char)* quote_char
|
||||
pair = st* key st* ":" st* value st*
|
||||
key = keychar*
|
||||
value = list | multilinetextblock | valuechar*
|
||||
|
||||
@@ -307,14 +301,6 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
res[key.ast] = value.ast ? value.ast.trim() : '';
|
||||
return res;
|
||||
},
|
||||
esc_quote_char(_1, quote) {
|
||||
// unescape
|
||||
return quote.sourceString;
|
||||
},
|
||||
quoted_key(disabled, _1, chars, _2) {
|
||||
// unquote
|
||||
return (disabled? disabled.sourceString : "") + chars.ast.join("");
|
||||
},
|
||||
key(chars) {
|
||||
return chars.sourceString ? chars.sourceString.trim() : '';
|
||||
},
|
||||
@@ -378,9 +364,6 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
tagend(_1, _2) {
|
||||
return '';
|
||||
},
|
||||
_terminal(){
|
||||
return this.sourceString;
|
||||
},
|
||||
multilinetextblockdelimiter(_) {
|
||||
return '';
|
||||
},
|
||||
|
||||
@@ -4,10 +4,6 @@ const { indentString } = require('./utils');
|
||||
|
||||
const enabled = (items = [], key = "enabled") => items.filter((item) => item[key]);
|
||||
const disabled = (items = [], key = "enabled") => items.filter((item) => !item[key]);
|
||||
const quoteKey = (key) => {
|
||||
const quotableChars = [':', '"', '{', '}', ' '];
|
||||
return quotableChars.some(char => key.includes(char)) ? ('"' + key.replaceAll('"', '\\"') + '"') : key;
|
||||
}
|
||||
|
||||
// remove the last line if two new lines are found
|
||||
const stripLastLine = (text) => {
|
||||
@@ -125,7 +121,7 @@ const jsonToBru = (json) => {
|
||||
if (enabled(queryParams).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(queryParams)
|
||||
.map((item) => `${quoteKey(item.name)}: ${item.value}`)
|
||||
.map((item) => `${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -133,7 +129,7 @@ const jsonToBru = (json) => {
|
||||
if (disabled(queryParams).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(queryParams)
|
||||
.map((item) => `~${quoteKey(item.name)}: ${item.value}`)
|
||||
.map((item) => `~${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -155,7 +151,7 @@ const jsonToBru = (json) => {
|
||||
if (enabled(headers).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(headers)
|
||||
.map((item) => `${quoteKey(item.name)}: ${item.value}`)
|
||||
.map((item) => `${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -163,7 +159,7 @@ const jsonToBru = (json) => {
|
||||
if (disabled(headers).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(headers)
|
||||
.map((item) => `~${quoteKey(item.name)}: ${item.value}`)
|
||||
.map((item) => `~${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -250,7 +246,7 @@ ${indentString(`domain: ${auth?.ntlm?.domain || ''}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
if (auth && auth.oauth2) {
|
||||
switch (auth?.oauth2?.grantType) {
|
||||
@@ -500,14 +496,14 @@ ${indentString(body.sparql)}
|
||||
|
||||
if (enabled(body.formUrlEncoded).length) {
|
||||
const enabledValues = enabled(body.formUrlEncoded)
|
||||
.map((item) => `${quoteKey(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${item.name}: ${getValueString(item.value)}`)
|
||||
.join('\n');
|
||||
bru += `${indentString(enabledValues)}\n`;
|
||||
}
|
||||
|
||||
if (disabled(body.formUrlEncoded).length) {
|
||||
const disabledValues = disabled(body.formUrlEncoded)
|
||||
.map((item) => `~${quoteKey(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `~${item.name}: ${getValueString(item.value)}`)
|
||||
.join('\n');
|
||||
bru += `${indentString(disabledValues)}\n`;
|
||||
}
|
||||
@@ -528,7 +524,7 @@ ${indentString(body.sparql)}
|
||||
item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';
|
||||
|
||||
if (item.type === 'text') {
|
||||
return `${enabled}${quoteKey(item.name)}: ${getValueString(item.value)}${contentType}`;
|
||||
return `${enabled}${item.name}: ${getValueString(item.value)}${contentType}`;
|
||||
}
|
||||
|
||||
if (item.type === 'file') {
|
||||
@@ -536,7 +532,7 @@ ${indentString(body.sparql)}
|
||||
const filestr = filepaths.join('|');
|
||||
|
||||
const value = `@file(${filestr})`;
|
||||
return `${enabled}${quoteKey(item.name)}: ${value}${contentType}`;
|
||||
return `${enabled}${item.name}: ${value}${contentType}`;
|
||||
}
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
@@ -81,25 +81,6 @@ headers {
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should parse single header with empty key', () => {
|
||||
const input = `
|
||||
headers {
|
||||
: world
|
||||
}`;
|
||||
|
||||
const output = parser(input);
|
||||
const expected = {
|
||||
headers: [
|
||||
{
|
||||
name: '',
|
||||
value: 'world',
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should parse multi headers', () => {
|
||||
const input = `
|
||||
headers {
|
||||
|
||||
@@ -17,11 +17,6 @@ get {
|
||||
params:query {
|
||||
apiKey: secret
|
||||
numbers: 998877665
|
||||
"key with spaces": is allowed
|
||||
"colon:parameter": is allowed
|
||||
"nested escaped \"quote\"": is allowed
|
||||
"{braces}": is allowed
|
||||
~"disabled:colon:parameter": is allowed
|
||||
~message: hello
|
||||
}
|
||||
|
||||
@@ -32,11 +27,6 @@ params:path {
|
||||
headers {
|
||||
content-type: application/json
|
||||
Authorization: Bearer 123
|
||||
"key with spaces": is allowed
|
||||
"colon:header": is allowed
|
||||
"{braces}": is allowed
|
||||
"nested escaped \"quote\"": is allowed
|
||||
~"disabled:colon:header": is allowed
|
||||
~transaction-id: {{transactionId}}
|
||||
}
|
||||
|
||||
@@ -114,23 +104,13 @@ body:sparql {
|
||||
body:form-urlencoded {
|
||||
apikey: secret
|
||||
numbers: +91998877665
|
||||
"key with spaces": is allowed
|
||||
"colon:parameter": is allowed
|
||||
"nested escaped \"quote\"": is allowed
|
||||
"{braces}": is allowed
|
||||
~message: hello
|
||||
~"disabled colon:parameter": is allowed
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
apikey: secret
|
||||
numbers: +91998877665
|
||||
"key with spaces": is allowed
|
||||
"colon:part": is allowed
|
||||
"nested escaped \"quote\"": is allowed
|
||||
"{braces}": is allowed
|
||||
~message: hello
|
||||
~"disabled colon:part": is allowed
|
||||
}
|
||||
|
||||
body:file {
|
||||
|
||||
115
packages/bruno-lang/v2/tests/fixtures/request.json
vendored
115
packages/bruno-lang/v2/tests/fixtures/request.json
vendored
@@ -24,36 +24,6 @@
|
||||
"type": "query",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "key with spaces",
|
||||
"value": "is allowed",
|
||||
"type": "query",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name" : "colon:parameter",
|
||||
"value" : "is allowed",
|
||||
"type": "query",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name" : "nested escaped \"quote\"",
|
||||
"value" : "is allowed",
|
||||
"type": "query",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "{braces}",
|
||||
"value": "is allowed",
|
||||
"type": "query",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name" : "disabled:colon:parameter",
|
||||
"value" : "is allowed",
|
||||
"type": "query",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"value": "hello",
|
||||
@@ -78,31 +48,6 @@
|
||||
"value": "Bearer 123",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "key with spaces",
|
||||
"value": "is allowed",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "colon:header",
|
||||
"value": "is allowed",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "{braces}",
|
||||
"value": "is allowed",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "nested escaped \"quote\"",
|
||||
"value": "is allowed",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "disabled:colon:header",
|
||||
"value": "is allowed",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"name": "transaction-id",
|
||||
"value": "{{transactionId}}",
|
||||
@@ -173,35 +118,10 @@
|
||||
"value": "+91998877665",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "key with spaces",
|
||||
"value": "is allowed",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "colon:parameter",
|
||||
"value": "is allowed",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "nested escaped \"quote\"",
|
||||
"value": "is allowed",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "{braces}",
|
||||
"value": "is allowed",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"value": "hello",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"name": "disabled colon:parameter",
|
||||
"value": "is allowed",
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"multipartForm": [
|
||||
@@ -219,47 +139,12 @@
|
||||
"enabled": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"contentType": "",
|
||||
"name": "key with spaces",
|
||||
"value": "is allowed",
|
||||
"enabled": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"contentType": "",
|
||||
"name": "colon:part",
|
||||
"value": "is allowed",
|
||||
"enabled": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"contentType": "",
|
||||
"name": "nested escaped \"quote\"",
|
||||
"value": "is allowed",
|
||||
"enabled": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"contentType": "",
|
||||
"name": "{braces}",
|
||||
"value": "is allowed",
|
||||
"enabled": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"contentType": "",
|
||||
"name": "message",
|
||||
"value": "hello",
|
||||
"enabled": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"contentType": "",
|
||||
"name": "disabled colon:part",
|
||||
"value": "is allowed",
|
||||
"enabled": false,
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"file" : [
|
||||
|
||||
@@ -89,16 +89,9 @@ export function addDigestInterceptor(axiosInstance, request) {
|
||||
authDetails.algorithm = 'MD5';
|
||||
}
|
||||
|
||||
// Build full URL from the original request (may include query params and baseURL)
|
||||
const resolvedUrl = new URL(
|
||||
originalRequest.url || request.url,
|
||||
originalRequest.baseURL || request.baseURL || 'http://localhost'
|
||||
);
|
||||
const uri = `${resolvedUrl.pathname}${resolvedUrl.search}`;
|
||||
// Used 'GET' as default method to avoid missing method error
|
||||
const method = (originalRequest.method || request.method || 'GET').toUpperCase();
|
||||
const uri = new URL(request.url, request.baseURL || 'http://localhost').pathname; // Handle relative URLs
|
||||
const HA1 = md5(`${username}:${authDetails.realm}:${password}`);
|
||||
const HA2 = md5(`${method}:${uri}`);
|
||||
const HA2 = md5(`${request.method}:${uri}`);
|
||||
const response = md5(
|
||||
`${HA1}:${authDetails.nonce}:${nonceCount}:${cnonce}:auth:${HA2}`
|
||||
);
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
const axios = require('axios');
|
||||
const { addDigestInterceptor } = require('./digestauth-helper');
|
||||
|
||||
describe('Digest Auth with query params', () => {
|
||||
test('uri should include path and query string', async () => {
|
||||
const axiosInstance = axios.create();
|
||||
|
||||
let callCount = 0;
|
||||
let capturedAuthorization;
|
||||
|
||||
// Custom adapter to simulate a 401 challenge then a 200 success
|
||||
axiosInstance.defaults.adapter = async (config) => {
|
||||
callCount += 1;
|
||||
if (callCount === 1) {
|
||||
const error = new Error('Unauthorized');
|
||||
error.config = config;
|
||||
error.response = {
|
||||
status: 401,
|
||||
headers: {
|
||||
'www-authenticate': 'Digest realm="test", nonce="abc", qop="auth"'
|
||||
}
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Second call should have Authorization header set by interceptor
|
||||
capturedAuthorization = config.headers && (config.headers.Authorization || config.headers.authorization);
|
||||
return {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config,
|
||||
data: { ok: true }
|
||||
};
|
||||
};
|
||||
|
||||
const request = {
|
||||
method: 'GET',
|
||||
url: 'http://example.com/resource?foo=bar&baz=qux',
|
||||
headers: {},
|
||||
digestConfig: { username: 'user', password: 'pass' }
|
||||
};
|
||||
|
||||
addDigestInterceptor(axiosInstance, request);
|
||||
|
||||
const res = await axiosInstance(request);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
expect(capturedAuthorization).toBeTruthy();
|
||||
// Extract uri="..." from the header
|
||||
const uriMatch = /uri="([^"]+)"/.exec(capturedAuthorization);
|
||||
expect(uriMatch).toBeTruthy();
|
||||
const uri = uriMatch[1];
|
||||
|
||||
// Expected to include both pathname and query
|
||||
expect(uri).toBe('/resource?foo=bar&baz=qux');
|
||||
});
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
meta {
|
||||
name: Redirect Cookie Save
|
||||
type: http
|
||||
seq: 9
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://httpbun.com/mix/s=302/c=foo:bar/r=https%3A%2F%2Fhttpbun.org%2Fget
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
tests {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
const cookieData = await jar.getCookie(
|
||||
"https://httpbun.com",
|
||||
"foo"
|
||||
);
|
||||
|
||||
test("should store redirect cookie under initial request domain", function () {
|
||||
expect(cookieData).to.not.be.undefined;
|
||||
expect(cookieData.key).to.equal("foo");
|
||||
expect(cookieData.value).to.equal("bar");
|
||||
});
|
||||
|
||||
jar.clear();
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
@@ -38,28 +38,9 @@ assert {
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
const brunoBirthDate = new Date('2019-08-08');
|
||||
|
||||
const calculateAgeFromBirthDate = (birthDate = brunoBirthDate) => {
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - birthDate.getFullYear();
|
||||
|
||||
const hasBirthdayPassedThisYear =
|
||||
today.getMonth() > birthDate.getMonth() ||
|
||||
(today.getMonth() === birthDate.getMonth() && today.getDate() >= birthDate.getDate());
|
||||
|
||||
if (!hasBirthdayPassedThisYear) {
|
||||
age--;
|
||||
}
|
||||
|
||||
return age;
|
||||
};
|
||||
|
||||
const brunoAge = calculateAgeFromBirthDate(brunoBirthDate);
|
||||
|
||||
bru.setVar("rUser", {
|
||||
full_name: 'Bruno',
|
||||
age: brunoAge,
|
||||
age: 5,
|
||||
'fav-food': ['egg', 'meat'],
|
||||
'want.attention': true
|
||||
});
|
||||
@@ -67,27 +48,8 @@ script:pre-request {
|
||||
|
||||
tests {
|
||||
test("should return json", function() {
|
||||
const brunoBirthDate = new Date('2019-08-08');
|
||||
|
||||
const calculateAgeFromBirthDate = (birthDate = brunoBirthDate) => {
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - birthDate.getFullYear();
|
||||
|
||||
const hasBirthdayPassedThisYear =
|
||||
today.getMonth() > birthDate.getMonth() ||
|
||||
(today.getMonth() === birthDate.getMonth() && today.getDate() >= birthDate.getDate());
|
||||
|
||||
if (!hasBirthdayPassedThisYear) {
|
||||
age--;
|
||||
}
|
||||
|
||||
return age;
|
||||
};
|
||||
|
||||
const brunoAge = calculateAgeFromBirthDate(brunoBirthDate);
|
||||
|
||||
const expectedResponse = `Hi, I am Bruno,
|
||||
I am ${brunoAge} years old.
|
||||
I am 5 years old.
|
||||
My favorite food is egg and meat.
|
||||
I like attention: true`;
|
||||
expect(res.getBody()).to.equal(expectedResponse);
|
||||
|
||||
@@ -8,20 +8,19 @@
|
||||
"name": "@usebruno/test-collection",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^8.4.1"
|
||||
"@faker-js/faker": "^8.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@faker-js/faker": {
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz",
|
||||
"integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==",
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.0.tgz",
|
||||
"integrity": "sha512-htW87352wzUCdX1jyUQocUcmAaFqcR/w082EC8iP/gtkF0K+aKcBp0hR5Arb7dzR8tQ1TrhE9DNa5EbJELm84w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fakerjs"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
"npm": ">=6.14.13"
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
"name": "@usebruno/test-collection",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^8.4.1"
|
||||
"@faker-js/faker": "^8.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user