From a04d434f7689b382a4a5cb31d0ce7141504c40cf Mon Sep 17 00:00:00 2001 From: prateek-bruno Date: Tue, 28 Apr 2026 11:23:16 +0530 Subject: [PATCH] feat: add new parameter "apiKeyHeaderName" for "apikey" mode (#7762) --- .../bruno-cli/src/runner/interpolate-vars.js | 3 ++ .../bruno-cli/src/runner/prepare-request.js | 2 + .../tests/runner/interpolate-vars.spec.js | 31 +++++++++++ .../tests/runner/prepare-request.spec.js | 6 +++ .../bruno-electron/src/ipc/network/index.js | 11 ++-- .../src/ipc/network/interpolate-vars.js | 3 ++ .../src/ipc/network/prepare-request.js | 2 + packages/bruno-js/src/bruno-request.js | 4 ++ .../tests/bruno-request-auth-mode.spec.js | 53 +++++++++++++++++++ tests/auth/apikey/apikey-runner.spec.ts | 20 +++++++ .../apikey-auth-mode-test/bruno.json | 5 ++ .../dynamic-header-key.bru | 29 ++++++++++ .../dynamic-query-key.bru | 29 ++++++++++ .../apikey/init-user-data/preferences.json | 11 ++++ 14 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 packages/bruno-cli/tests/runner/interpolate-vars.spec.js create mode 100644 packages/bruno-js/tests/bruno-request-auth-mode.spec.js create mode 100644 tests/auth/apikey/apikey-runner.spec.ts create mode 100644 tests/auth/apikey/fixtures/collections/apikey-auth-mode-test/bruno.json create mode 100644 tests/auth/apikey/fixtures/collections/apikey-auth-mode-test/dynamic-header-key.bru create mode 100644 tests/auth/apikey/fixtures/collections/apikey-auth-mode-test/dynamic-query-key.bru create mode 100644 tests/auth/apikey/init-user-data/preferences.json diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index bacd22b73..0b19e0a79 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -65,6 +65,9 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc delete request.headers[key]; request.headers[_interpolate(key)] = _interpolate(value); }); + if (request.apiKeyHeaderName) { + request.apiKeyHeaderName = _interpolate(request.apiKeyHeaderName); + } const contentType = getContentType(request.headers); const isGraphqlRequest = request.mode === 'graphql'; diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 861288f55..bfd2cd51c 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -67,6 +67,7 @@ const prepareRequest = async (item = {}, collection = {}) => { if (collectionAuth.mode === 'apikey') { if (collectionAuth.apikey?.placement === 'header') { axiosRequest.headers[collectionAuth.apikey?.key] = collectionAuth.apikey?.value; + axiosRequest.apiKeyHeaderName = collectionAuth.apikey?.key; } if (collectionAuth.apikey?.placement === 'queryparams') { @@ -309,6 +310,7 @@ const prepareRequest = async (item = {}, collection = {}) => { if (request.auth.mode === 'apikey') { if (request.auth.apikey?.placement === 'header') { axiosRequest.headers[request.auth.apikey?.key] = request.auth.apikey?.value; + axiosRequest.apiKeyHeaderName = request.auth.apikey?.key; } if (request.auth.apikey?.placement === 'queryparams') { diff --git a/packages/bruno-cli/tests/runner/interpolate-vars.spec.js b/packages/bruno-cli/tests/runner/interpolate-vars.spec.js new file mode 100644 index 000000000..7349b5bdd --- /dev/null +++ b/packages/bruno-cli/tests/runner/interpolate-vars.spec.js @@ -0,0 +1,31 @@ +const { describe, it, expect } = require('@jest/globals'); +const interpolateVars = require('../../src/runner/interpolate-vars'); + +describe('interpolate-vars: api key header name sidecar', () => { + it('interpolates apiKeyHeaderName in lockstep with interpolated header keys', () => { + const request = { + url: 'https://example.com', + mode: 'none', + headers: { + '{{api_header_name}}': '{{api_key_value}}' + }, + apiKeyHeaderName: '{{api_header_name}}', + pathParams: [] + }; + + interpolateVars( + request, + { + api_header_name: 'X-API-Key', + api_key_value: 'secret-key-value' + }, + {}, + {} + ); + + expect(request.headers).toEqual({ + 'X-API-Key': 'secret-key-value' + }); + expect(request.apiKeyHeaderName).toEqual('X-API-Key'); + }); +}); diff --git a/packages/bruno-cli/tests/runner/prepare-request.spec.js b/packages/bruno-cli/tests/runner/prepare-request.spec.js index c48141afd..27515eb51 100644 --- a/packages/bruno-cli/tests/runner/prepare-request.spec.js +++ b/packages/bruno-cli/tests/runner/prepare-request.spec.js @@ -72,6 +72,7 @@ describe('prepare-request: prepareRequest', () => { const result = await prepareRequest(item, collection); expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}'); + expect(result.apiKeyHeaderName).toEqual('x-api-key'); }); it('If collection auth is apikey in header and request has existing headers', async () => { @@ -88,6 +89,7 @@ describe('prepare-request: prepareRequest', () => { const result = await prepareRequest(item, collection); expect(result.headers).toHaveProperty('Content-Type', 'application/json'); expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}'); + expect(result.apiKeyHeaderName).toEqual('x-api-key'); }); it('If collection auth is apikey in query parameters', async () => { @@ -106,6 +108,7 @@ describe('prepare-request: prepareRequest', () => { const expected = urlObj.toString(); const result = await prepareRequest(item, collection); expect(result.url).toEqual(expected); + expect(result.apiKeyHeaderName).toBeUndefined(); }); }); @@ -355,6 +358,7 @@ describe('prepare-request: prepareRequest', () => { const result = await prepareRequest(item); expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}'); + expect(result.apiKeyHeaderName).toEqual('x-api-key'); }); it('If request auth is apikey in header and request has existing headers', async () => { @@ -371,6 +375,7 @@ describe('prepare-request: prepareRequest', () => { const result = await prepareRequest(item); expect(result.headers).toHaveProperty('Content-Type', 'application/json'); expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}'); + expect(result.apiKeyHeaderName).toEqual('x-api-key'); }); it('If request auth is apikey in query parameters', async () => { @@ -389,6 +394,7 @@ describe('prepare-request: prepareRequest', () => { const expected = urlObj.toString(); const result = await prepareRequest(item); expect(result.url).toEqual(expected); + expect(result.apiKeyHeaderName).toBeUndefined(); }); }); diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 14475aa45..1445a0979 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -240,7 +240,7 @@ const configureRequest = async ( const url = new URL(request.url); url.searchParams.set(tokenQueryKey, tokenValue); request.url = url.toString(); - } catch (error) {} + } catch (error) { } } } break; @@ -257,7 +257,7 @@ const configureRequest = async ( const url = new URL(request.url); url.searchParams.set(tokenQueryKey, tokenValue); request.url = url.toString(); - } catch (error) {} + } catch (error) { } } } break; @@ -274,7 +274,7 @@ const configureRequest = async ( const url = new URL(request.url); url.searchParams.set(tokenQueryKey, tokenValue); request.url = url.toString(); - } catch (error) {} + } catch (error) { } } } break; @@ -291,7 +291,7 @@ const configureRequest = async ( const url = new URL(request.url); url.searchParams.set(tokenQueryKey, tokenValue); request.url = url.toString(); - } catch (error) {} + } catch (error) { } } } break; @@ -355,9 +355,6 @@ const configureRequest = async ( request.url = urlObj.toString(); } - // Remove apiKeyAuthValueForQueryParams, already interpolated and added to URL - delete request.apiKeyAuthValueForQueryParams; - return axiosInstance; }; diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index b95475e61..a90cc74a5 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -72,6 +72,9 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc delete request.headers[key]; request.headers[_interpolate(key)] = _interpolate(value); }); + if (request.apiKeyHeaderName) { + request.apiKeyHeaderName = _interpolate(request.apiKeyHeaderName); + } const contentType = getContentType(request.headers); const isGraphqlRequest = request.mode === 'graphql'; diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index df08f156e..4c8ab28b3 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -85,6 +85,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { if (apiKeyAuth.key.length === 0) break; if (apiKeyAuth.placement === 'header') { axiosRequest.headers[apiKeyAuth.key] = apiKeyAuth.value; + axiosRequest.apiKeyHeaderName = apiKeyAuth.key; } else if (apiKeyAuth.placement === 'queryparams') { // If the API key authentication is set and its placement is 'queryparams', add it to the axios request object. This will be used in the configureRequest function to append the API key to the query parameters of the request URL. axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth; @@ -338,6 +339,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { if (apiKeyAuth.key.length === 0) break; if (apiKeyAuth.placement === 'header') { axiosRequest.headers[apiKeyAuth.key] = apiKeyAuth.value; + axiosRequest.apiKeyHeaderName = apiKeyAuth.key; } else if (apiKeyAuth.placement === 'queryparams') { // If the API key authentication is set and its placement is 'queryparams', add it to the axios request object. This will be used in the configureRequest function to append the API key to the query parameters of the request URL. axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth; diff --git a/packages/bruno-js/src/bruno-request.js b/packages/bruno-js/src/bruno-request.js index a57ae41be..ddf0a22e5 100644 --- a/packages/bruno-js/src/bruno-request.js +++ b/packages/bruno-js/src/bruno-request.js @@ -102,6 +102,10 @@ class BrunoRequest { return 'bearer'; } else if (this.headers?.['Authorization']?.startsWith('Basic') || this.req?.auth?.username) { return 'basic'; + } else if (this.req?.apiKeyAuthValueForQueryParams) { + return 'apikey'; + } else if (this.req?.apiKeyHeaderName && this.headers?.[this.req.apiKeyHeaderName] !== undefined) { + return 'apikey'; } else if (this.req?.awsv4) { return 'awsv4'; } else if (this.req?.digestConfig) { diff --git a/packages/bruno-js/tests/bruno-request-auth-mode.spec.js b/packages/bruno-js/tests/bruno-request-auth-mode.spec.js new file mode 100644 index 000000000..bd3e815fa --- /dev/null +++ b/packages/bruno-js/tests/bruno-request-auth-mode.spec.js @@ -0,0 +1,53 @@ +const { describe, it, expect } = require('@jest/globals'); +const BrunoRequest = require('../src/bruno-request'); + +const makeReq = (overrides = {}) => ({ + url: 'http://localhost:5000/api', + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...overrides.headers + }, + data: undefined, + ...overrides +}); + +describe('BrunoRequest - getAuthMode()', () => { + it('returns apikey for header placement when the api key header is present', () => { + const req = new BrunoRequest( + makeReq({ + headers: { 'X-API-Key': 'secret' }, + apiKeyHeaderName: 'X-API-Key' + }) + ); + + expect(req.getAuthMode()).toBe('apikey'); + }); + + it('returns none when pre-request script deletes the api key header', () => { + const req = new BrunoRequest( + makeReq({ + headers: { 'X-API-Key': 'secret' }, + apiKeyHeaderName: 'X-API-Key' + }) + ); + + req.deleteHeader('X-API-Key'); + + expect(req.getAuthMode()).toBe('none'); + }); + + it('returns apikey for queryparams placement marker', () => { + const req = new BrunoRequest( + makeReq({ + apiKeyAuthValueForQueryParams: { + key: 'api_key', + value: 'secret', + placement: 'queryparams' + } + }) + ); + + expect(req.getAuthMode()).toBe('apikey'); + }); +}); diff --git a/tests/auth/apikey/apikey-runner.spec.ts b/tests/auth/apikey/apikey-runner.spec.ts new file mode 100644 index 000000000..02052afdb --- /dev/null +++ b/tests/auth/apikey/apikey-runner.spec.ts @@ -0,0 +1,20 @@ +import { test } from '../../../playwright'; +import { setSandboxMode, runCollection, validateRunnerResults } from '../../utils/page'; + +const COLLECTION_NAME = 'apikey-auth-mode-test'; + +test.describe.serial('API Key Auth Mode Runner', () => { + for (const mode of ['safe', 'developer'] as const) { + test(`detects API key auth in ${mode} mode`, async ({ pageWithUserData: page }) => { + await setSandboxMode(page, COLLECTION_NAME, mode); + await runCollection(page, COLLECTION_NAME); + + await validateRunnerResults(page, { + totalRequests: 2, + passed: 2, + failed: 0, + skipped: 0 + }); + }); + } +}); diff --git a/tests/auth/apikey/fixtures/collections/apikey-auth-mode-test/bruno.json b/tests/auth/apikey/fixtures/collections/apikey-auth-mode-test/bruno.json new file mode 100644 index 000000000..7294b29af --- /dev/null +++ b/tests/auth/apikey/fixtures/collections/apikey-auth-mode-test/bruno.json @@ -0,0 +1,5 @@ +{ + "version": "1", + "name": "apikey-auth-mode-test", + "type": "collection" +} diff --git a/tests/auth/apikey/fixtures/collections/apikey-auth-mode-test/dynamic-header-key.bru b/tests/auth/apikey/fixtures/collections/apikey-auth-mode-test/dynamic-header-key.bru new file mode 100644 index 000000000..f0944fd8b --- /dev/null +++ b/tests/auth/apikey/fixtures/collections/apikey-auth-mode-test/dynamic-header-key.bru @@ -0,0 +1,29 @@ +meta { + name: dynamic-header-key + type: http + seq: 1 +} + +get { + url: http://localhost:8081/ping + body: none + auth: apikey +} + +auth:apikey { + key: {{api_key_header_name}} + value: {{api_key_value}} + placement: header +} + +vars:pre-request { + api_key_header_name: X-API-Key + api_key_value: secret-key-value +} + +tests { + test("detects API key auth when the header key is interpolated", function() { + expect(req.getAuthMode()).to.equal("apikey"); + expect(req.getHeader("X-API-Key")).to.equal("secret-key-value"); + }); +} diff --git a/tests/auth/apikey/fixtures/collections/apikey-auth-mode-test/dynamic-query-key.bru b/tests/auth/apikey/fixtures/collections/apikey-auth-mode-test/dynamic-query-key.bru new file mode 100644 index 000000000..ba40c0b2b --- /dev/null +++ b/tests/auth/apikey/fixtures/collections/apikey-auth-mode-test/dynamic-query-key.bru @@ -0,0 +1,29 @@ +meta { + name: dynamic-query-key + type: http + seq: 2 +} + +get { + url: http://localhost:8081/ping + body: none + auth: apikey +} + +auth:apikey { + key: {{api_key_query_name}} + value: {{api_key_value}} + placement: queryparams +} + +vars:pre-request { + api_key_query_name: api_key + api_key_value: secret-key-value +} + +tests { + test("detects API key auth when the query key is interpolated", function() { + expect(req.getAuthMode()).to.equal("apikey"); + expect(req.getUrl()).to.contain("api_key=secret-key-value"); + }); +} diff --git a/tests/auth/apikey/init-user-data/preferences.json b/tests/auth/apikey/init-user-data/preferences.json new file mode 100644 index 000000000..e0d1590a5 --- /dev/null +++ b/tests/auth/apikey/init-user-data/preferences.json @@ -0,0 +1,11 @@ +{ + "lastOpenedCollections": [ + "{{collectionPath}}/apikey-auth-mode-test" + ], + "preferences": { + "onboarding": { + "hasLaunchedBefore": true, + "hasSeenWelcomeModal": true + } + } +}