Skip to content

Commit 4fa882c

Browse files
authored
fix: 3014 - Postman import ignores API Key "in" placement setting and defaults to header (usebruno#7968)
* fix: 3014 - Postman import ignores API Key "in" placement setting and defaults to header * addressed review comments * addressed review comment * addressed review comment - added a test id * addressed reveiew comment
1 parent 2c9dc9d commit 4fa882c

11 files changed

Lines changed: 382 additions & 7 deletions

File tree

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ const ModalFooter = ({
2828
confirmDisabled,
2929
hideCancel,
3030
hideFooter,
31-
confirmButtonColor = 'primary'
31+
confirmButtonColor = 'primary',
32+
dataTestId = 'modal'
3233
}) => {
3334
confirmText = confirmText || 'Save';
3435
cancelText = cancelText || 'Cancel';
@@ -51,6 +52,7 @@ const ModalFooter = ({
5152
disabled={confirmDisabled}
5253
onClick={handleSubmit}
5354
className="submit"
55+
data-testid={`${dataTestId}-submit-btn`}
5456
>
5557
{confirmText}
5658
</Button>
@@ -151,6 +153,7 @@ const Modal = ({
151153
hideCancel={hideCancel}
152154
hideFooter={hideFooter}
153155
confirmButtonColor={confirmButtonColor}
156+
dataTestId={dataTestId}
154157
/>
155158
</div>
156159

packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
2525

2626
const Icon = forwardRef((props, ref) => {
2727
return (
28-
<div ref={ref} className="flex items-center justify-end auth-type-label select-none">
28+
<div ref={ref} data-testid="auth-placement-label" className="flex items-center justify-end auth-type-label select-none">
2929
{humanizeRequestAPIKeyPlacement(apikeyAuth?.placement)}
3030
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
3131
</div>
@@ -89,7 +89,7 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
8989
</div>
9090

9191
<label className="block mb-1">Add To</label>
92-
<div className="inline-flex items-center cursor-pointer auth-placement-selector w-fit">
92+
<div data-testid="auth-placement-selector" className="inline-flex items-center cursor-pointer auth-placement-selector w-fit">
9393
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
9494
<div
9595
className="dropdown-item"

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
7070

7171
{errorMessage && (
7272
<div
73+
data-testid="import-error-message"
7374
className="mb-4 p-2 border rounded-md"
7475
style={{
7576
backgroundColor: theme.status.danger.background,

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,11 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format, sour
261261
) : null}
262262

263263
<div className="mt-1">
264-
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
264+
<span
265+
data-testid="import-collection-browse-link"
266+
className="text-link cursor-pointer hover:underline"
267+
onClick={browse}
268+
>
265269
Browse
266270
</span>
267271
</div>

packages/bruno-converters/src/postman/postman-to-bruno.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ export const processAuth = (auth, requestObject, isCollection = false) => {
267267
requestObject.auth.apikey = {
268268
key: ensureString(authValues.key),
269269
value: ensureString(authValues.value),
270-
placement: 'header' // By default we are placing the apikey values in headers!
270+
placement: authValues.in === 'query' ? 'queryparams' : 'header' // map Postman `in` to Bruno placement; defaults to header
271271
};
272272
break;
273273
case AUTH_TYPES.DIGEST:

packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,4 +580,67 @@ describe('Request Authentication', () => {
580580
basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null, oauth1: null
581581
});
582582
});
583+
584+
describe('API Key Auth placement', () => {
585+
const buildApiKeyCollection = (apikey) => ({
586+
info: {
587+
name: 'API Key Collection',
588+
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
589+
},
590+
item: [
591+
{
592+
name: 'API Key Request',
593+
request: {
594+
method: 'GET',
595+
url: 'https://api.example.com/test',
596+
auth: { type: 'apikey', apikey }
597+
}
598+
}
599+
]
600+
});
601+
602+
it('should map Postman in=query to Bruno placement=queryparams', async () => {
603+
const postmanCollection = buildApiKeyCollection([
604+
{ key: 'key', value: 'X-API-Key' },
605+
{ key: 'value', value: 'secret-token' },
606+
{ key: 'in', value: 'query' }
607+
]);
608+
609+
const result = await postmanToBruno(postmanCollection);
610+
611+
expect(result.items[0].request.auth.mode).toBe('apikey');
612+
expect(result.items[0].request.auth.apikey).toEqual({
613+
key: 'X-API-Key',
614+
value: 'secret-token',
615+
placement: 'queryparams'
616+
});
617+
});
618+
619+
it('should map Postman in=header to Bruno placement=header', async () => {
620+
const postmanCollection = buildApiKeyCollection([
621+
{ key: 'key', value: 'X-API-Key' },
622+
{ key: 'value', value: 'secret-token' },
623+
{ key: 'in', value: 'header' }
624+
]);
625+
626+
const result = await postmanToBruno(postmanCollection);
627+
628+
expect(result.items[0].request.auth.apikey).toEqual({
629+
key: 'X-API-Key',
630+
value: 'secret-token',
631+
placement: 'header'
632+
});
633+
});
634+
635+
it('should default placement to header when Postman in is absent', async () => {
636+
const postmanCollection = buildApiKeyCollection([
637+
{ key: 'key', value: 'X-API-Key' },
638+
{ key: 'value', value: 'secret-token' }
639+
]);
640+
641+
const result = await postmanToBruno(postmanCollection);
642+
643+
expect(result.items[0].request.auth.apikey.placement).toBe('header');
644+
});
645+
});
583646
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"info": {
3+
"_postman_id": "6df527f0-77aa-4b67-957f-8a3f0c96a027",
4+
"name": "My Collection",
5+
"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/)",
6+
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
7+
"_exporter_id": "54316404",
8+
"_collection_link": "https://go.postman.co/collection/54316404-6df527f0-77aa-4b67-957f-8a3f0c96a027?source=collection_link"
9+
},
10+
"item": [
11+
{
12+
"name": "Headers with API Key",
13+
"request": {
14+
"auth": {
15+
"type": "apikey",
16+
"apikey": [
17+
{
18+
"key": "in",
19+
"value": "header",
20+
"type": "string"
21+
},
22+
{
23+
"key": "value",
24+
"value": "hello",
25+
"type": "string"
26+
},
27+
{
28+
"key": "key",
29+
"value": "hii",
30+
"type": "string"
31+
}
32+
]
33+
},
34+
"method": "GET",
35+
"header": [],
36+
"url": {
37+
"raw": "https://api.com/users?X-API-KEY=12345",
38+
"protocol": "https",
39+
"host": [
40+
"api",
41+
"com"
42+
],
43+
"path": [
44+
"users"
45+
],
46+
"query": [
47+
{
48+
"key": "X-API-KEY",
49+
"value": "12345"
50+
}
51+
]
52+
}
53+
},
54+
"response": []
55+
}
56+
]
57+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"info": {
3+
"_postman_id": "6df527f0-77aa-4b67-957f-8a3f0c96a027",
4+
"name": "My Collection",
5+
"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/)",
6+
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
7+
"_exporter_id": "54316404",
8+
"_collection_link": "https://go.postman.co/collection/54316404-6df527f0-77aa-4b67-957f-8a3f0c96a027?source=collection_link"
9+
},
10+
"item": [
11+
{
12+
"name": "Query with API Key",
13+
"request": {
14+
"auth": {
15+
"type": "apikey",
16+
"apikey": [
17+
{
18+
"key": "in",
19+
"value": "query",
20+
"type": "string"
21+
},
22+
{
23+
"key": "value",
24+
"value": "hello",
25+
"type": "string"
26+
},
27+
{
28+
"key": "key",
29+
"value": "hii",
30+
"type": "string"
31+
}
32+
]
33+
},
34+
"method": "GET",
35+
"header": [],
36+
"url": {
37+
"raw": "https://api.com/users?X-API-KEY=12345",
38+
"protocol": "https",
39+
"host": [
40+
"api",
41+
"com"
42+
],
43+
"path": [
44+
"users"
45+
],
46+
"query": [
47+
{
48+
"key": "X-API-KEY",
49+
"value": "12345"
50+
}
51+
]
52+
}
53+
},
54+
"response": []
55+
}
56+
]
57+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { test, expect } from '../../../playwright';
2+
import * as path from 'path';
3+
import { closeAllCollections, openCollection, selectRequestPaneTab } from '../../utils/page';
4+
import { buildCommonLocators } from '../../utils/page/locators';
5+
6+
test.describe('Import Postman Collection with API Key in Header', () => {
7+
let originalShowOpenDialog;
8+
9+
test.beforeAll(async ({ electronApp }) => {
10+
await electronApp.evaluate(({ dialog }) => {
11+
originalShowOpenDialog = dialog.showOpenDialog;
12+
});
13+
});
14+
15+
test.afterAll(async ({ electronApp, page }) => {
16+
await closeAllCollections(page);
17+
await electronApp.evaluate(({ dialog }) => {
18+
dialog.showOpenDialog = originalShowOpenDialog;
19+
});
20+
});
21+
22+
test('should import Postman collection with API Key in Header successfully', async ({ page, electronApp, createTmpDir }) => {
23+
const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-import-apikey-header-collection.json');
24+
const locators = buildCommonLocators(page);
25+
26+
const importDir = await createTmpDir('imported-collection');
27+
28+
await electronApp.evaluate(({ dialog }, { importDir }) => {
29+
dialog.showOpenDialog = async () => ({
30+
canceled: false,
31+
filePaths: [importDir]
32+
});
33+
}, { importDir });
34+
35+
await test.step('Open import collection modal', async () => {
36+
await locators.plusMenu.button().click();
37+
await locators.plusMenu.importCollection().click();
38+
});
39+
40+
await test.step('Wait for import modal and verify title', async () => {
41+
const importModal = page.getByRole('dialog');
42+
await importModal.waitFor({ state: 'visible' });
43+
await expect(locators.modal.title('Import Collection')).toBeVisible();
44+
});
45+
46+
await test.step('Upload Postman collection file using hidden file input', async () => {
47+
await locators.import.fileInput().setInputFiles(postmanFile);
48+
await locators.import.locationModal().waitFor({ state: 'visible', timeout: 10000 });
49+
});
50+
51+
await test.step('Verify no parsing errors occurred', async () => {
52+
const hasError = await locators.import.parsingError().isVisible().catch(() => false);
53+
if (hasError) {
54+
throw new Error('Collection import failed with parsing error');
55+
}
56+
});
57+
58+
await test.step('Verify location selection modal appears', async () => {
59+
await expect(locators.modal.title('Import Collection')).toBeVisible();
60+
});
61+
62+
await test.step('Verify collection name appears in location modal', async () => {
63+
await expect(locators.import.locationModal().getByText('My Collection')).toBeVisible();
64+
});
65+
66+
await test.step('Click Browse link to select collection folder', async () => {
67+
await locators.import.browseLink(locators.import.locationModal()).click();
68+
});
69+
70+
await test.step('Complete import by clicking import button', async () => {
71+
const locationModal = locators.import.locationModal();
72+
await locators.import.importButton(locationModal).click();
73+
await locationModal.waitFor({ state: 'hidden' });
74+
});
75+
76+
await test.step('Open collection and verify request is displayed', async () => {
77+
await openCollection(page, 'My Collection');
78+
await expect(locators.sidebar.collection('My Collection')).toBeVisible();
79+
await expect(locators.sidebar.request('Headers with API Key')).toBeVisible();
80+
await locators.sidebar.request('Headers with API Key').click();
81+
await expect(locators.request.pane()).toBeVisible();
82+
});
83+
84+
await test.step('Verify API key is set to Header within Auth section', async () => {
85+
await selectRequestPaneTab(page, 'Auth');
86+
await expect(locators.auth.apiKey.placementSelector()).toBeVisible();
87+
await expect(locators.auth.apiKey.placementLabel()).toHaveText('Header');
88+
});
89+
});
90+
});

0 commit comments

Comments
 (0)