Skip to content

Commit 43e05a0

Browse files
committed
fix(import): address coderabbit comments
1 parent d564b23 commit 43e05a0

7 files changed

Lines changed: 140 additions & 88 deletions

File tree

packages/bruno-app/src/components/ShareCollection/index.js

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,6 @@ const ShareCollection = ({ onClose, collectionUid }) => {
6565

6666
const handlePostmanModalClose = () => {
6767
setShowPostmanExportModal(false);
68-
};
69-
70-
const handlePostmanExported = () => {
71-
setShowPostmanExportModal(false);
7268
onClose();
7369
};
7470

@@ -100,6 +96,16 @@ const ShareCollection = ({ onClose, collectionUid }) => {
10096

10197
const isDisabled = isCollectionLoading || isExporting;
10298

99+
if (showPostmanExportModal) {
100+
return (
101+
<ExportToPostman
102+
collection={collection}
103+
onClose={handlePostmanModalClose}
104+
onExported={handlePostmanModalClose}
105+
/>
106+
);
107+
}
108+
103109
return (
104110
<>
105111
<Modal size="lg" title="Share Collection" handleCancel={onClose} hideFooter>
@@ -207,14 +213,6 @@ const ShareCollection = ({ onClose, collectionUid }) => {
207213
</div>
208214
</StyledWrapper>
209215
</Modal>
210-
211-
{showPostmanExportModal && (
212-
<ExportToPostman
213-
collection={collection}
214-
onClose={handlePostmanModalClose}
215-
onExported={handlePostmanExported}
216-
/>
217-
)}
218216
</>
219217
);
220218
};

packages/bruno-app/src/components/Sidebar/Collections/Collection/ExportCollection/ExportToPostman/index.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as Yup from 'yup';
55
import { cloneDeep } from 'lodash';
66
import { IconCaretDown } from '@tabler/icons';
77
import toast from 'react-hot-toast';
8-
import { sanitizeName } from 'utils/common/regex';
8+
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
99
import Portal from 'components/Portal';
1010
import Modal from 'components/Modal';
1111
import Dropdown from 'components/Dropdown';
@@ -20,6 +20,7 @@ const ExportToPostman = ({ onClose, onExported, collection }) => {
2020
const inputRef = useRef();
2121
const [preserveScripts, setPreserveScripts] = useState(false);
2222
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
23+
const [isExporting, setIsExporting] = useState(false);
2324
const optionsDropdownTippyRef = useRef();
2425
const onOptionsDropdownCreate = (ref) => (optionsDropdownTippyRef.current = ref);
2526

@@ -31,8 +32,13 @@ const ExportToPostman = ({ onClose, onExported, collection }) => {
3132
},
3233
validationSchema: Yup.object({
3334
fileName: Yup.string()
35+
.trim()
3436
.min(1, 'must be at least 1 character')
3537
.max(255, 'must be 255 characters or less')
38+
.test('is-valid-name', function (value) {
39+
const isValid = validateName(value);
40+
return isValid ? true : this.createError({ message: validateNameError(value) });
41+
})
3642
.required('Name is required'),
3743
location: Yup.string().min(1, 'Location is required').required('Location is required')
3844
}),
@@ -43,9 +49,12 @@ const ExportToPostman = ({ onClose, onExported, collection }) => {
4349
const fileExists = formik.errors.fileName === FILE_EXISTS_ERROR;
4450

4551
async function handleExport(values, overwrite) {
52+
if (isExporting) return;
53+
54+
setIsExporting(true);
4655
try {
4756
const content = exportPostmanCollection(cloneDeep(collection), { preserveScripts });
48-
await dispatch(exportCollectionToPostman(values.location, `${values.fileName}.json`, content, overwrite));
57+
await dispatch(exportCollectionToPostman(values.location, `${values.fileName.trim()}.json`, content, overwrite));
4958
toast.success('Collection exported successfully');
5059
onExported();
5160
} catch (error) {
@@ -57,6 +66,8 @@ const ExportToPostman = ({ onClose, onExported, collection }) => {
5766
return;
5867
}
5968
toast.error('Failed to export collection: ' + message);
69+
} finally {
70+
setIsExporting(false);
6071
}
6172
}
6273

@@ -102,6 +113,7 @@ const ExportToPostman = ({ onClose, onExported, collection }) => {
102113
dataTestId="export-to-postman-modal"
103114
confirmText={fileExists ? 'Replace' : 'Export'}
104115
confirmButtonColor={fileExists ? 'danger' : 'primary'}
116+
confirmDisabled={isExporting}
105117
handleConfirm={() => (fileExists ? handleReplace() : formik.handleSubmit())}
106118
handleCancel={onClose}
107119
footerLeft={(
@@ -182,7 +194,7 @@ const ExportToPostman = ({ onClose, onExported, collection }) => {
182194
<div>
183195
<span className="preserve-scripts-label">Preserve scripts</span>
184196
<p className="preserve-scripts-description">
185-
Exports bru.* scripts as-is, without translating them to pm.*.
197+
Export Bruno scripts without translating them.
186198
</p>
187199
</div>
188200
</label>

packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format, sour
373373
<div>
374374
<span className="checkbox-option-label">Preserve scripts</span>
375375
<p className="checkbox-option-description">
376-
Imports pm.* scripts as-is, without translating them to bru.*.
376+
Import Postman scripts without translating them.
377377
</p>
378378
</div>
379379
</label>

packages/bruno-electron/src/ipc/collection.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2555,7 +2555,12 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
25552555
throw new Error('Export location does not exist');
25562556
}
25572557

2558-
const filePath = path.join(dirPath, fileName);
2558+
// ensure the resolved path is inside the export directory
2559+
const resolvedDir = path.resolve(dirPath);
2560+
const filePath = path.resolve(resolvedDir, fileName);
2561+
if (!filePath.startsWith(resolvedDir + path.sep) && filePath !== resolvedDir) {
2562+
throw new Error('Invalid file name');
2563+
}
25592564

25602565
if (!overwrite && fs.existsSync(filePath)) {
25612566
throw new Error(`path: ${filePath} already exists`);

tests/export/postman/export-preserve-scripts.spec.ts

Lines changed: 35 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,16 @@
1-
import { test, expect, Page } from '../../../playwright';
2-
import { closeAllCollections, openCollection } from '../../utils/page';
3-
import { buildCommonLocators } from '../../utils/page/locators';
1+
import { test, expect } from '../../../playwright';
2+
import {
3+
closeAllCollections,
4+
buildCommonLocators,
5+
openExportToPostmanModal,
6+
closeExportToPostmanModal,
7+
exportCollectionToPostman
8+
} from '../../utils/page';
49
import * as fs from 'fs';
510
import * as nodePath from 'path';
611

712
const COLLECTION_NAME = 'Export Scripts Collection';
813

9-
const openExportToPostmanModal = async (page: Page) => {
10-
const locators = buildCommonLocators(page);
11-
12-
await openCollection(page, COLLECTION_NAME);
13-
14-
const collectionAction = locators.actions.collectionActions(COLLECTION_NAME);
15-
await locators.sidebar.collection(COLLECTION_NAME).hover();
16-
await expect(collectionAction).toBeVisible({ timeout: 2000 });
17-
await collectionAction.click();
18-
await locators.dropdown.item('Share').click();
19-
await expect(locators.modal.title('Share Collection')).toBeVisible();
20-
21-
await page.getByTestId('export-format-postman').click();
22-
await locators.modal.button('Proceed').click();
23-
await expect(locators.modal.title('Export to Postman')).toBeVisible();
24-
};
25-
26-
// export a collection via Share -> Export to Postman into outputDir and read the file
27-
const exportPostmanCollection = async (
28-
page: Page,
29-
outputDir: string,
30-
{ preserveScripts }: { preserveScripts: boolean }
31-
) => {
32-
const locators = buildCommonLocators(page);
33-
34-
await openExportToPostmanModal(page);
35-
36-
await test.step('Set the export location', async () => {
37-
await page.getByLabel('Location', { exact: true }).fill(outputDir);
38-
});
39-
40-
await test.step('Configure preserve scripts', async () => {
41-
if (preserveScripts) {
42-
// 'Preserve scripts' lives under the modal footer's advanced options
43-
await page.getByRole('button', { name: 'Options' }).click();
44-
await page.getByTestId('show-advanced-options-toggle').click();
45-
const checkbox = page.getByTestId('preserve-scripts-toggle');
46-
await expect(checkbox).toBeVisible();
47-
await checkbox.check();
48-
await expect(checkbox).toBeChecked();
49-
}
50-
});
51-
52-
return await test.step('Export and read the written file', async () => {
53-
await locators.modal.button('Export').click();
54-
const filePath = nodePath.join(outputDir, `${COLLECTION_NAME}.json`);
55-
await expect.poll(() => fs.existsSync(filePath), { timeout: 5000 }).toBe(true);
56-
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
57-
});
58-
};
59-
6014
const findLoginEvents = (exported: any) => {
6115
const login = exported.item.find((i: any) => i.name === 'Login');
6216
return {
@@ -66,13 +20,13 @@ const findLoginEvents = (exported: any) => {
6620
};
6721

6822
test.describe('Export Postman Collection - Preserve scripts option', () => {
69-
test.afterAll(async ({ pageWithUserData: page }) => {
23+
test.afterEach(async ({ pageWithUserData: page }) => {
7024
await closeAllCollections(page);
7125
});
7226

7327
test('should export scripts as is when preserve scripts is enabled', async ({ pageWithUserData: page, createTmpDir }) => {
7428
const outputDir = await createTmpDir('postman-export');
75-
const exported = await exportPostmanCollection(page, outputDir, { preserveScripts: true });
29+
const exported = await exportCollectionToPostman(page, COLLECTION_NAME, outputDir, { preserveScripts: true });
7630
const { prerequest, test: testEvent } = findLoginEvents(exported);
7731

7832
expect(prerequest.script.exec.join('\n')).toContain('bru.setEnvVar(\'token\', \'abc\')');
@@ -83,7 +37,7 @@ test.describe('Export Postman Collection - Preserve scripts option', () => {
8337

8438
test('should translate scripts to pm.* by default when preserve scripts is disabled', async ({ pageWithUserData: page, createTmpDir }) => {
8539
const outputDir = await createTmpDir('postman-export');
86-
const exported = await exportPostmanCollection(page, outputDir, { preserveScripts: false });
40+
const exported = await exportCollectionToPostman(page, COLLECTION_NAME, outputDir, { preserveScripts: false });
8741
const { prerequest } = findLoginEvents(exported);
8842

8943
expect(prerequest.script.exec.join('\n')).toContain('pm.environment.set');
@@ -98,10 +52,10 @@ test.describe('Export Postman Collection - Preserve scripts option', () => {
9852
// Pre seed a conflicting file so the export hits the "already exists" path
9953
fs.writeFileSync(filePath, '{"stale":true}', 'utf-8');
10054

101-
await openExportToPostmanModal(page);
55+
await openExportToPostmanModal(page, COLLECTION_NAME);
10256

10357
await test.step('Exporting over an existing file prompts to replace', async () => {
104-
await page.getByLabel('Location', { exact: true }).fill(outputDir);
58+
await locators.export.locationInput().fill(outputDir);
10559
await locators.modal.button('Export').click();
10660
await expect(page.getByText('Name already exists in this location')).toBeVisible();
10761
await expect(locators.modal.button('Replace')).toBeVisible();
@@ -123,10 +77,10 @@ test.describe('Export Postman Collection - Preserve scripts option', () => {
12377
test('should require a name and location before exporting', async ({ pageWithUserData: page }) => {
12478
const locators = buildCommonLocators(page);
12579

126-
await openExportToPostmanModal(page);
80+
await openExportToPostmanModal(page, COLLECTION_NAME);
12781

12882
await test.step('Clearing the name and location blocks export with validation errors', async () => {
129-
await page.getByLabel('Name', { exact: true }).fill('');
83+
await locators.export.nameInput().fill('');
13084
await locators.modal.button('Export').click();
13185

13286
await expect(page.getByText('Name is required')).toBeVisible();
@@ -135,13 +89,24 @@ test.describe('Export Postman Collection - Preserve scripts option', () => {
13589
await expect(locators.modal.title('Export to Postman')).toBeVisible();
13690
});
13791

138-
// Close both modals to cleanup the app state for the next test
139-
await test.step('Close the export and share modals', async () => {
140-
await locators.export.postmanModal().getByTestId('modal-close-button').click();
141-
await expect(locators.modal.title('Export to Postman')).toBeHidden();
142-
await locators.modal.byTitle('Share Collection').getByTestId('modal-close-button').click();
143-
await expect(locators.modal.title('Share Collection')).toBeHidden();
92+
await closeExportToPostmanModal(page);
93+
});
94+
95+
test('should block export when the name has invalid characters', async ({ pageWithUserData: page }) => {
96+
const locators = buildCommonLocators(page);
97+
98+
await openExportToPostmanModal(page, COLLECTION_NAME);
99+
100+
await test.step('An invalid name shows a validation error and blocks export', async () => {
101+
await locators.export.nameInput().fill('foo/bar');
102+
await locators.modal.button('Export').click();
103+
104+
await expect(page.getByText('Special characters aren\'t allowed in the name')).toBeVisible();
105+
// The modal stays open and the export did not proceed
106+
await expect(locators.modal.title('Export to Postman')).toBeVisible();
144107
});
108+
109+
await closeExportToPostmanModal(page);
145110
});
146111

147112
test('should let the user rename to avoid overwriting an existing file', async ({ pageWithUserData: page, createTmpDir }) => {
@@ -151,17 +116,17 @@ test.describe('Export Postman Collection - Preserve scripts option', () => {
151116
// Pre-seed a conflicting file for the default (collection) name
152117
fs.writeFileSync(nodePath.join(outputDir, `${COLLECTION_NAME}.json`), '{"stale":true}', 'utf-8');
153118

154-
await openExportToPostmanModal(page);
119+
await openExportToPostmanModal(page, COLLECTION_NAME);
155120

156121
await test.step('Exporting over an existing file prompts to replace', async () => {
157-
await page.getByLabel('Location', { exact: true }).fill(outputDir);
122+
await locators.export.locationInput().fill(outputDir);
158123
await locators.modal.button('Export').click();
159124
await expect(page.getByText('Name already exists in this location')).toBeVisible();
160125
await expect(locators.modal.button('Replace')).toBeVisible();
161126
});
162127

163128
await test.step('Renaming clears the conflict and restores Export', async () => {
164-
await page.getByLabel('Name', { exact: true }).fill('Renamed Export');
129+
await locators.export.nameInput().fill('Renamed Export');
165130
await expect(page.getByText('Name already exists in this location')).toBeHidden();
166131
await expect(locators.modal.button('Export')).toBeVisible();
167132
});

tests/utils/page/actions.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { test, expect, Page, Locator, ElectronApplication, waitForReadyPage as waitForReadyPageImpl } from '../../../playwright';
22
import process from 'node:process';
33
import * as path from 'path';
4+
import * as fs from 'fs';
45
import { buildCommonLocators, buildScriptErrorLocators, buildGrpcCommonLocators, buildWebsocketCommonLocators } from './locators';
56
import { waitForCollectionMount } from './mounting';
67

@@ -2145,6 +2146,68 @@ const generateCollectionDocs = async (
21452146
});
21462147
};
21472148

2149+
const openExportToPostmanModal = async (page: Page, collectionName: string) => {
2150+
await test.step(`Open Export to Postman for "${collectionName}"`, async () => {
2151+
const locators = buildCommonLocators(page);
2152+
2153+
await openCollection(page, collectionName);
2154+
2155+
const collectionAction = locators.actions.collectionActions(collectionName);
2156+
await locators.sidebar.collection(collectionName).hover();
2157+
await expect(collectionAction).toBeVisible({ timeout: 2000 });
2158+
await collectionAction.click();
2159+
await locators.dropdown.item('Share').click();
2160+
await expect(locators.modal.title('Share Collection')).toBeVisible();
2161+
2162+
await locators.export.postmanFormatCard().click();
2163+
await locators.modal.button('Proceed').click();
2164+
await expect(locators.modal.title('Export to Postman')).toBeVisible();
2165+
});
2166+
};
2167+
2168+
const closeExportToPostmanModal = async (page: Page) => {
2169+
await test.step('Close the export modal', async () => {
2170+
const locators = buildCommonLocators(page);
2171+
2172+
await locators.export.postmanModal().getByTestId('modal-close-button').click();
2173+
await expect(locators.modal.title('Export to Postman')).toBeHidden();
2174+
await expect(locators.modal.title('Share Collection')).toBeHidden();
2175+
});
2176+
};
2177+
2178+
const exportCollectionToPostman = async (
2179+
page: Page,
2180+
collectionName: string,
2181+
outputDir: string,
2182+
{ preserveScripts = false }: { preserveScripts?: boolean } = {}
2183+
) => {
2184+
const locators = buildCommonLocators(page);
2185+
2186+
await openExportToPostmanModal(page, collectionName);
2187+
2188+
await test.step('Set the export location', async () => {
2189+
await locators.export.locationInput().fill(outputDir);
2190+
});
2191+
2192+
await test.step('Configure preserve scripts', async () => {
2193+
if (preserveScripts) {
2194+
await locators.export.optionsButton().click();
2195+
await locators.export.advancedOptionsToggle().click();
2196+
const checkbox = locators.export.preserveScriptsToggle();
2197+
await expect(checkbox).toBeVisible();
2198+
await checkbox.check();
2199+
await expect(checkbox).toBeChecked();
2200+
}
2201+
});
2202+
2203+
return await test.step('Export and read the written file', async () => {
2204+
await locators.modal.button('Export').click();
2205+
const filePath = path.join(outputDir, `${collectionName}.json`);
2206+
await expect.poll(() => fs.existsSync(filePath), { timeout: 5000 }).toBe(true);
2207+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
2208+
});
2209+
};
2210+
21482211
/**
21492212
* Toggle the "Enable App" request setting idempotently (Settings tab).
21502213
* Enabling exposes the App tab and the Request/App/File view-mode toggle.
@@ -2482,6 +2545,9 @@ export {
24822545
openRequestInFolder,
24832546
setUrlEncoding,
24842547
generateCollectionDocs,
2548+
openExportToPostmanModal,
2549+
closeExportToPostmanModal,
2550+
exportCollectionToPostman,
24852551
openFolderSettings,
24862552
setTableRowDescriptionValue,
24872553
setAppCode,

0 commit comments

Comments
 (0)