diff --git a/packages/bruno-app/src/components/Modal/index.js b/packages/bruno-app/src/components/Modal/index.js index 36ca7a4fc..75662d2bc 100644 --- a/packages/bruno-app/src/components/Modal/index.js +++ b/packages/bruno-app/src/components/Modal/index.js @@ -28,7 +28,8 @@ const ModalFooter = ({ confirmDisabled, hideCancel, hideFooter, - confirmButtonColor = 'primary' + confirmButtonColor = 'primary', + dataTestId = 'modal' }) => { confirmText = confirmText || 'Save'; cancelText = cancelText || 'Cancel'; @@ -51,6 +52,7 @@ const ModalFooter = ({ disabled={confirmDisabled} onClick={handleSubmit} className="submit" + data-testid={`${dataTestId}-submit-btn`} > {confirmText} @@ -151,6 +153,7 @@ const Modal = ({ hideCancel={hideCancel} hideFooter={hideFooter} confirmButtonColor={confirmButtonColor} + dataTestId={dataTestId} /> diff --git a/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js index 7d5d02f55..74b89a2af 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js @@ -25,7 +25,7 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => { const Icon = forwardRef((props, ref) => { return ( -
+
{humanizeRequestAPIKeyPlacement(apikeyAuth?.placement)}
@@ -89,7 +89,7 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
-
+
} placement="bottom-end">
{ {errorMessage && (
- + Browse
diff --git a/packages/bruno-converters/src/postman/postman-to-bruno.js b/packages/bruno-converters/src/postman/postman-to-bruno.js index 277ec4bf5..eed246102 100644 --- a/packages/bruno-converters/src/postman/postman-to-bruno.js +++ b/packages/bruno-converters/src/postman/postman-to-bruno.js @@ -267,7 +267,7 @@ export const processAuth = (auth, requestObject, isCollection = false) => { requestObject.auth.apikey = { key: ensureString(authValues.key), value: ensureString(authValues.value), - placement: 'header' // By default we are placing the apikey values in headers! + placement: authValues.in === 'query' ? 'queryparams' : 'header' // map Postman `in` to Bruno placement; defaults to header }; break; case AUTH_TYPES.DIGEST: diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js index ca483f066..b6080f82d 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js @@ -580,4 +580,67 @@ describe('Request Authentication', () => { basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null, oauth1: null }); }); + + describe('API Key Auth placement', () => { + const buildApiKeyCollection = (apikey) => ({ + info: { + name: 'API Key Collection', + schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' + }, + item: [ + { + name: 'API Key Request', + request: { + method: 'GET', + url: 'https://api.example.com/test', + auth: { type: 'apikey', apikey } + } + } + ] + }); + + it('should map Postman in=query to Bruno placement=queryparams', async () => { + const postmanCollection = buildApiKeyCollection([ + { key: 'key', value: 'X-API-Key' }, + { key: 'value', value: 'secret-token' }, + { key: 'in', value: 'query' } + ]); + + const result = await postmanToBruno(postmanCollection); + + expect(result.items[0].request.auth.mode).toBe('apikey'); + expect(result.items[0].request.auth.apikey).toEqual({ + key: 'X-API-Key', + value: 'secret-token', + placement: 'queryparams' + }); + }); + + it('should map Postman in=header to Bruno placement=header', async () => { + const postmanCollection = buildApiKeyCollection([ + { key: 'key', value: 'X-API-Key' }, + { key: 'value', value: 'secret-token' }, + { key: 'in', value: 'header' } + ]); + + const result = await postmanToBruno(postmanCollection); + + expect(result.items[0].request.auth.apikey).toEqual({ + key: 'X-API-Key', + value: 'secret-token', + placement: 'header' + }); + }); + + it('should default placement to header when Postman in is absent', async () => { + const postmanCollection = buildApiKeyCollection([ + { key: 'key', value: 'X-API-Key' }, + { key: 'value', value: 'secret-token' } + ]); + + const result = await postmanToBruno(postmanCollection); + + expect(result.items[0].request.auth.apikey.placement).toBe('header'); + }); + }); }); diff --git a/tests/import/postman/fixtures/postman-import-apikey-header-collection.json b/tests/import/postman/fixtures/postman-import-apikey-header-collection.json new file mode 100644 index 000000000..164c54ded --- /dev/null +++ b/tests/import/postman/fixtures/postman-import-apikey-header-collection.json @@ -0,0 +1,57 @@ +{ + "info": { + "_postman_id": "6df527f0-77aa-4b67-957f-8a3f0c96a027", + "name": "My Collection", + "description": "### Welcome to Postman! This is your first collection. \n\nCollections are your starting point for building and testing APIs. You can use this one to:\n\n• Group related requests\n• Test your API in real-world scenarios\n• Document and share your requests\n\nUpdate the name and overview whenever you’re ready to make it yours.\n\n[Learn more about Postman Collections.](https://learning.postman.com/docs/collections/collections-overview/)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "54316404", + "_collection_link": "https://go.postman.co/collection/54316404-6df527f0-77aa-4b67-957f-8a3f0c96a027?source=collection_link" + }, + "item": [ + { + "name": "Headers with API Key", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "in", + "value": "header", + "type": "string" + }, + { + "key": "value", + "value": "hello", + "type": "string" + }, + { + "key": "key", + "value": "hii", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://api.com/users?X-API-KEY=12345", + "protocol": "https", + "host": [ + "api", + "com" + ], + "path": [ + "users" + ], + "query": [ + { + "key": "X-API-KEY", + "value": "12345" + } + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/tests/import/postman/fixtures/postman-import-apikey-query-collection.json b/tests/import/postman/fixtures/postman-import-apikey-query-collection.json new file mode 100644 index 000000000..091575bf2 --- /dev/null +++ b/tests/import/postman/fixtures/postman-import-apikey-query-collection.json @@ -0,0 +1,57 @@ +{ + "info": { + "_postman_id": "6df527f0-77aa-4b67-957f-8a3f0c96a027", + "name": "My Collection", + "description": "### Welcome to Postman! This is your first collection. \n\nCollections are your starting point for building and testing APIs. You can use this one to:\n\n• Group related requests\n• Test your API in real-world scenarios\n• Document and share your requests\n\nUpdate the name and overview whenever you’re ready to make it yours.\n\n[Learn more about Postman Collections.](https://learning.postman.com/docs/collections/collections-overview/)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "54316404", + "_collection_link": "https://go.postman.co/collection/54316404-6df527f0-77aa-4b67-957f-8a3f0c96a027?source=collection_link" + }, + "item": [ + { + "name": "Query with API Key", + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "in", + "value": "query", + "type": "string" + }, + { + "key": "value", + "value": "hello", + "type": "string" + }, + { + "key": "key", + "value": "hii", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://api.com/users?X-API-KEY=12345", + "protocol": "https", + "host": [ + "api", + "com" + ], + "path": [ + "users" + ], + "query": [ + { + "key": "X-API-KEY", + "value": "12345" + } + ] + } + }, + "response": [] + } + ] +} diff --git a/tests/import/postman/import-apikey-header-collection.spec.ts b/tests/import/postman/import-apikey-header-collection.spec.ts new file mode 100644 index 000000000..63f70d0ad --- /dev/null +++ b/tests/import/postman/import-apikey-header-collection.spec.ts @@ -0,0 +1,90 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; +import { closeAllCollections, openCollection, selectRequestPaneTab } from '../../utils/page'; +import { buildCommonLocators } from '../../utils/page/locators'; + +test.describe('Import Postman Collection with API Key in Header', () => { + let originalShowOpenDialog; + + test.beforeAll(async ({ electronApp }) => { + await electronApp.evaluate(({ dialog }) => { + originalShowOpenDialog = dialog.showOpenDialog; + }); + }); + + test.afterAll(async ({ electronApp, page }) => { + await closeAllCollections(page); + await electronApp.evaluate(({ dialog }) => { + dialog.showOpenDialog = originalShowOpenDialog; + }); + }); + + test('should import Postman collection with API Key in Header successfully', async ({ page, electronApp, createTmpDir }) => { + const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-import-apikey-header-collection.json'); + const locators = buildCommonLocators(page); + + const importDir = await createTmpDir('imported-collection'); + + await electronApp.evaluate(({ dialog }, { importDir }) => { + dialog.showOpenDialog = async () => ({ + canceled: false, + filePaths: [importDir] + }); + }, { importDir }); + + await test.step('Open import collection modal', async () => { + await locators.plusMenu.button().click(); + await locators.plusMenu.importCollection().click(); + }); + + await test.step('Wait for import modal and verify title', async () => { + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(locators.modal.title('Import Collection')).toBeVisible(); + }); + + await test.step('Upload Postman collection file using hidden file input', async () => { + await locators.import.fileInput().setInputFiles(postmanFile); + await locators.import.locationModal().waitFor({ state: 'visible', timeout: 10000 }); + }); + + await test.step('Verify no parsing errors occurred', async () => { + const hasError = await locators.import.parsingError().isVisible().catch(() => false); + if (hasError) { + throw new Error('Collection import failed with parsing error'); + } + }); + + await test.step('Verify location selection modal appears', async () => { + await expect(locators.modal.title('Import Collection')).toBeVisible(); + }); + + await test.step('Verify collection name appears in location modal', async () => { + await expect(locators.import.locationModal().getByText('My Collection')).toBeVisible(); + }); + + await test.step('Click Browse link to select collection folder', async () => { + await locators.import.browseLink(locators.import.locationModal()).click(); + }); + + await test.step('Complete import by clicking import button', async () => { + const locationModal = locators.import.locationModal(); + await locators.import.importButton(locationModal).click(); + await locationModal.waitFor({ state: 'hidden' }); + }); + + await test.step('Open collection and verify request is displayed', async () => { + await openCollection(page, 'My Collection'); + await expect(locators.sidebar.collection('My Collection')).toBeVisible(); + await expect(locators.sidebar.request('Headers with API Key')).toBeVisible(); + await locators.sidebar.request('Headers with API Key').click(); + await expect(locators.request.pane()).toBeVisible(); + }); + + await test.step('Verify API key is set to Header within Auth section', async () => { + await selectRequestPaneTab(page, 'Auth'); + await expect(locators.auth.apiKey.placementSelector()).toBeVisible(); + await expect(locators.auth.apiKey.placementLabel()).toHaveText('Header'); + }); + }); +}); diff --git a/tests/import/postman/import-apikey-query-collection.spec.ts b/tests/import/postman/import-apikey-query-collection.spec.ts new file mode 100644 index 000000000..ba178593f --- /dev/null +++ b/tests/import/postman/import-apikey-query-collection.spec.ts @@ -0,0 +1,90 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; +import { closeAllCollections, openCollection, selectRequestPaneTab } from '../../utils/page'; +import { buildCommonLocators } from '../../utils/page/locators'; + +test.describe('Import Postman Collection with API Key in Query Params', () => { + let originalShowOpenDialog; + + test.beforeAll(async ({ electronApp }) => { + await electronApp.evaluate(({ dialog }) => { + originalShowOpenDialog = dialog.showOpenDialog; + }); + }); + + test.afterAll(async ({ electronApp, page }) => { + await closeAllCollections(page); + await electronApp.evaluate(({ dialog }) => { + dialog.showOpenDialog = originalShowOpenDialog; + }); + }); + + test('should import Postman collection with API Key in Query Params successfully', async ({ page, electronApp, createTmpDir }) => { + const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-import-apikey-query-collection.json'); + const locators = buildCommonLocators(page); + + const importDir = await createTmpDir('imported-collection'); + + await electronApp.evaluate(({ dialog }, { importDir }) => { + dialog.showOpenDialog = async () => ({ + canceled: false, + filePaths: [importDir] + }); + }, { importDir }); + + await test.step('Open import collection modal', async () => { + await locators.plusMenu.button().click(); + await locators.plusMenu.importCollection().click(); + }); + + await test.step('Wait for import modal and verify title', async () => { + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(locators.modal.title('Import Collection')).toBeVisible(); + }); + + await test.step('Upload Postman collection file using hidden file input', async () => { + await locators.import.fileInput().setInputFiles(postmanFile); + await locators.import.locationModal().waitFor({ state: 'visible', timeout: 10000 }); + }); + + await test.step('Verify no parsing errors occurred', async () => { + const hasError = await locators.import.parsingError().isVisible().catch(() => false); + if (hasError) { + throw new Error('Collection import failed with parsing error'); + } + }); + + await test.step('Verify location selection modal appears', async () => { + await expect(locators.modal.title('Import Collection')).toBeVisible(); + }); + + await test.step('Verify collection name appears in location modal', async () => { + await expect(locators.import.locationModal().getByText('My Collection')).toBeVisible(); + }); + + await test.step('Click Browse link to select collection folder', async () => { + await locators.import.browseLink(locators.import.locationModal()).click(); + }); + + await test.step('Complete import by clicking import button', async () => { + const locationModal = locators.import.locationModal(); + await locators.import.importButton(locationModal).click(); + await locationModal.waitFor({ state: 'hidden' }); + }); + + await test.step('Open collection and verify request is displayed', async () => { + await openCollection(page, 'My Collection'); + await expect(locators.sidebar.collection('My Collection')).toBeVisible(); + await expect(locators.sidebar.request('Query with API Key')).toBeVisible(); + await locators.sidebar.request('Query with API Key').click(); + await expect(locators.request.pane()).toBeVisible(); + }); + + await test.step('Verify API key is set to Query Params within Auth section', async () => { + await selectRequestPaneTab(page, 'Auth'); + await expect(locators.auth.apiKey.placementSelector()).toBeVisible(); + await expect(locators.auth.apiKey.placementLabel()).toHaveText('Query Params'); + }); + }); +}); diff --git a/tests/utils/page/locators.ts b/tests/utils/page/locators.ts index f1b86dc7c..953ceda2f 100644 --- a/tests/utils/page/locators.ts +++ b/tests/utils/page/locators.ts @@ -86,7 +86,14 @@ export const buildCommonLocators = (page: Page) => ({ requestTestId: () => page.getByTestId('request-name'), generateCodeButton: () => page.locator('#request-actions .infotip').first(), bodyModeSelector: () => page.getByTestId('request-body-mode-selector'), - bodyEditor: () => page.getByTestId('request-body-editor') + bodyEditor: () => page.getByTestId('request-body-editor'), + pane: () => page.getByTestId('request-pane') + }, + auth: { + apiKey: { + placementSelector: () => page.getByTestId('auth-placement-selector'), + placementLabel: () => page.getByTestId('auth-placement-label') + } }, tags: { input: () => page.getByTestId('tag-input').getByRole('textbox'), @@ -118,7 +125,10 @@ export const buildCommonLocators = (page: Page) => ({ locationModal: () => page.locator('[data-testid="import-collection-location-modal"]'), locationInput: () => page.locator('#collection-location'), fileInput: () => page.locator('input[type="file"]'), - envOption: (name: string) => page.locator('.dropdown-item').getByText(name, { exact: true }) + envOption: (name: string) => page.locator('.dropdown-item').getByText(name, { exact: true }), + parsingError: () => page.getByTestId('import-error-message'), + browseLink: (root?: Locator) => (root ?? page).getByTestId('import-collection-browse-link'), + importButton: (root?: Locator) => (root ?? page).getByTestId('import-collection-location-modal-submit-btn') }, /** * Build generic table locators for any table with a testId