Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ const Collection = ({ collection, searchText }) => {
const folderItems = sortByNameThenSequence(filter(collection.items, (i) => isItemAFolder(i)));

return (
<StyledWrapper className="flex flex-col">
<StyledWrapper className="flex flex-col" id={`collection-${collection.name.replace(/\s+/g, '-').toLowerCase()}`}>
{showNewRequestModal && <NewRequest collectionUid={collection.uid} onClose={() => setShowNewRequestModal(false)} />}
{showNewFolderModal && <NewFolder collectionUid={collection.uid} onClose={() => setShowNewFolderModal(false)} />}
{showRenameCollectionModal && (
Expand Down
29 changes: 26 additions & 3 deletions packages/bruno-converters/src/openapi/openapi-to-bruno.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,35 @@ const buildEmptyJsonBody = (bodySchema, visited = new Map()) => {
return _jsonBody;
};

const transformOpenapiRequestItem = (request) => {
const transformOpenapiRequestItem = (request, usedNames = new Set()) => {
let _operationObject = request.operationObject;

let operationName = _operationObject.summary || _operationObject.operationId || _operationObject.description;
if (!operationName) {
operationName = `${request.method} ${request.path}`;
}

// Sanitize operation name to prevent Bruno parsing issues
if (operationName) {
// Replace line breaks and normalize whitespace
operationName = operationName.replace(/[\r\n\s]+/g, ' ').trim();
}
if (usedNames.has(operationName)) {
// Make name unique to prevent filename collisions
// Try adding method info first
let uniqueName = `${operationName} (${request.method.toUpperCase()})`;

// If still not unique, add counter
let counter = 1;
while (usedNames.has(uniqueName)) {
uniqueName = `${operationName} (${counter})`;
counter++;
}

operationName = uniqueName;
}
usedNames.add(operationName);

// replace OpenAPI links in path by Bruno variables
let path = request.path.replace(/{([a-zA-Z]+)}/g, `{{${_operationObject.operationId}_$1}}`);

Expand Down Expand Up @@ -419,6 +440,8 @@ const openAPIRuntimeExpressionToScript = (expression) => {
};

export const parseOpenApiCollection = (data) => {
const usedNames = new Set();

const brunoCollection = {
name: '',
uid: uuid(),
Expand Down Expand Up @@ -512,11 +535,11 @@ export const parseOpenApiCollection = (data) => {
name: group.name
}
},
items: group.requests.map(transformOpenapiRequestItem)
items: group.requests.map(req => transformOpenapiRequestItem(req, usedNames)),
};
});

let ungroupedItems = ungroupedRequests.map(transformOpenapiRequestItem);
let ungroupedItems = ungroupedRequests.map(req => transformOpenapiRequestItem(req, usedNames));
let brunoCollectionItems = brunoFolders.concat(ungroupedItems);
brunoCollection.items = brunoCollectionItems;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ test.describe('Global Environment Import Tests', () => {
await page.getByText('×').click();

// Test GET request with global environment
await page.locator('.collection-item-name').first().click();
await page.locator('#collection-environment-test-collection .collection-item-name').first().click();
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts/{{userId}}');
await page.locator('[data-testid="send-arrow-icon"]').click();
await page.locator('[data-testid="response-status-code"]').waitFor({ state: 'visible' });
Expand All @@ -74,7 +74,7 @@ test.describe('Global Environment Import Tests', () => {
await expect(responsePane).toContainText('"userId": 1');

// Test POST request
await page.locator('.collection-item-name').nth(1).click();
await page.locator('#collection-environment-test-collection .collection-item-name').nth(1).click();
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts');
await page.locator('[data-testid="send-arrow-icon"]').click();
await page.locator('[data-testid="response-status-code"]').waitFor({ state: 'visible' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';

test.describe('Import Corrupted Bruno Collection - Should Fail', () => {
const testDataDir = path.join(__dirname, '../test-data');

test('Import Bruno collection with invalid JSON structure should fail', async ({ page }) => {
const brunoFile = path.join(testDataDir, 'bruno-malformed.json');
const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-malformed.json');

// go to welcome screen
await page.locator('.bruno-logo').click();

await page.getByRole('button', { name: 'Import Collection' }).click();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';

test.describe('Import Bruno Collection - Missing Required Schema Fields', () => {
const testDataDir = path.join(__dirname, '../test-data');

test('Import Bruno collection missing required version field should fail', async ({ page }) => {
const brunoFile = path.join(testDataDir, 'bruno-missing-required-fields.json');
const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-missing-required-fields.json');

await page.getByRole('button', { name: 'Import Collection' }).click();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ import * as path from 'path';


test.describe('Import Bruno Testbench Collection', () => {
const testDataDir = path.join(__dirname, '../test-data');

test.beforeAll(async ({ page }) => {
// Navigate back to homescreen after all tests
await page.locator('.bruno-logo').click();
});

test('Import Bruno Testbench collection successfully', async ({ page }) => {
const brunoFile = path.join(testDataDir, 'bruno-testbench.json');
const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-testbench.json');

await page.getByRole('button', { name: 'Import Collection' }).click();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';

test.describe('Invalid File Handling', () => {
const testDataDir = path.join(__dirname, '../test-data');

test('Handle invalid file without crashing', async ({ page }) => {
const invalidFile = path.join(testDataDir, 'invalid.txt');
const invalidFile = path.resolve(__dirname, 'fixtures', 'invalid.txt');

await page.getByRole('button', { name: 'Import Collection' }).click();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';

test.describe('Import Insomnia Collection v4', () => {
const testDataDir = path.join(__dirname, '../test-data');

test('Import Insomnia Collection v4 successfully', async ({ page }) => {
const insomniaFile = path.join(testDataDir, 'insomnia-v4.json');
const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v4.json');

await page.getByRole('button', { name: 'Import Collection' }).click();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';

test.describe('Import Insomnia Collection v5', () => {
const testDataDir = path.join(__dirname, '../test-data');

test('Import Insomnia Collection v5 successfully', async ({ page }) => {
const insomniaFile = path.join(testDataDir, 'insomnia-v5.yaml');
const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v5.yaml');

await page.getByRole('button', { name: 'Import Collection' }).click();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';

test.describe('Invalid Insomnia Collection - Missing Collection Array', () => {
const testDataDir = path.join(__dirname, '../test-data');

test('Handle Insomnia v5 collection missing collection array', async ({ page }) => {
const insomniaFile = path.join(testDataDir, 'insomnia-v5-invalid-missing-collection.yaml');
const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v5-invalid-missing-collection.yaml');

await page.getByRole('button', { name: 'Import Collection' }).click();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';

test.describe('Invalid Insomnia Collection - Malformed Structure', () => {
const testDataDir = path.join(__dirname, '../test-data');

test('Handle malformed Insomnia collection structure', async ({ page }) => {
const insomniaFile = path.join(testDataDir, 'insomnia-malformed.json');
const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-malformed.json');

await page.getByRole('button', { name: 'Import Collection' }).click();

Expand All @@ -20,7 +18,7 @@ test.describe('Invalid Insomnia Collection - Malformed Structure', () => {
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });

// Check for error message - this should fail during JSON parsing
const hasError = await page.getByText('Failed to parse the file').isVisible();
const hasError = await page.getByText('Failed to parse the file').first().isVisible();
expect(hasError).toBe(true);

// Cleanup: close any open modals
Expand Down
54 changes: 54 additions & 0 deletions tests/import/openapi/duplicate-operation-names-fix.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { test, expect } from '../../../playwright';
import * as path from 'path';

test.describe('OpenAPI Duplicate Names Handling', () => {
test('should handle duplicate operation names', async ({ newPage: page, createTmpDir }) => {
const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-duplicate-operation-name.yaml');

// start the import process
await page.getByRole('button', { name: 'Import Collection' }).click();

// wait for the import collection modal to appear
const importModal = page.locator('[data-testid="import-collection-modal"]');
await importModal.waitFor({ state: 'visible' });

// upload the OpenAPI file with duplicate operation names
await page.setInputFiles('input[type="file"]', openApiFile);

// wait for the file processing to complete
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });

// verify that the collection location modal appears (OpenAPI files go directly to location modal)
const locationModal = page.locator('[data-testid="import-collection-location-modal"]');
// verify the collection name is correctly parsed despite duplicate operation names
await expect(locationModal.getByText('Duplicate Test Collection')).toBeVisible();

// select a location
await page.locator('#collection-location').fill(await createTmpDir('duplicate-test'));
await page.getByRole('button', { name: 'Import', exact: true }).click();

// verify the collection was imported successfully
await expect(page.locator('#sidebar-collection-name').getByText('Duplicate Test Collection')).toBeVisible();

// configure the collection settings
await page.locator('#sidebar-collection-name').getByText('Duplicate Test Collection').click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();

// verify that all requests were imported correctly despite duplicate operation names
const requestCount = await page.locator('#collection-duplicate-test-collection .collection-item-name').count();
expect(requestCount).toBe(3);

// cleanup: close the collection
await page
.locator('.collection-name')
.filter({ has: page.locator('#sidebar-collection-name:has-text("Duplicate Test Collection")') })
.locator('.collection-actions')
.click();
await page.locator('.dropdown-item').getByText('Close').click();
await page.getByRole('button', { name: 'Close' }).click();

// return to home page
await page.locator('.bruno-logo').click();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
openapi: 3.0.0
info:
title: Duplicate Test Collection
version: 1.0.0
description: Test collection for handling duplicate operation names
servers:
- url: https://api.example.com
description: Example server
paths:
/users:
get:
summary: 'Get Users'
description: 'Get all users'
operationId: getUsers
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
post:
summary: 'Get Users'
description: 'Create a new user (same summary as GET)'
operationId: createUser
responses:
'201':
description: Created
content:
application/json:
schema:
type: object
/products:
get:
summary: 'Get Users'
description: 'Get all products (same summary as users GET)'
operationId: getProducts
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
openapi: 3.0.0
info:
title: Newline Test Collection
version: 1.0.0
description: Test collection for operation names with newlines
servers:
- url: https://api.example.com
description: Example server
paths:
/users:
get:
summary: "Get users\nwith newline"
description: 'This operation has newlines in the summary'
operationId: getUsersWithNewline
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
post:
summary: "Create user\n\nwith multiple\n\nnewlines"
description: 'This operation has multiple consecutive newlines'
operationId: createUserWithNewlines
responses:
'201':
description: Created
content:
application/json:
schema:
type: object
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';

test.describe('Import OpenAPI v3 JSON Collection', () => {
const testDataDir = path.join(__dirname, '../test-data');

test('Import simple OpenAPI v3 JSON successfully', async ({ page }) => {
const openApiFile = path.join(testDataDir, 'openapi-simple.json');
const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-simple.json');

await page.getByRole('button', { name: 'Import Collection' }).click();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';

test.describe('Import OpenAPI v3 YAML Collection', () => {
const testDataDir = path.join(__dirname, '../test-data');

test('Import comprehensive OpenAPI v3 YAML successfully', async ({ page }) => {
const openApiFile = path.join(testDataDir, 'openapi-comprehensive.yaml');

const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-comprehensive.yaml');

await page.getByRole('button', { name: 'Import Collection' }).click();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';

test.describe('Invalid OpenAPI - Malformed YAML', () => {
const testDataDir = path.join(__dirname, '../test-data');

test('Handle malformed OpenAPI YAML structure', async ({ page }) => {
const openApiFile = path.join(testDataDir, 'openapi-malformed.yaml');
const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-malformed.yaml');

await page.getByRole('button', { name: 'Import Collection' }).click();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { test, expect } from '../../../playwright';
import * as path from 'path';

test.describe('Invalid OpenAPI - Missing Info Section', () => {
const testDataDir = path.join(__dirname, '../test-data');

test('Handle OpenAPI specification missing required info section', async ({ page }) => {
const openApiFile = path.join(testDataDir, 'openapi-missing-info.yaml');
const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-missing-info.yaml');

await page.getByRole('button', { name: 'Import Collection' }).click();

Expand Down
Loading
Loading