From 2de7ba0d0cd5ccf164466bc1b8db24c3d04f119a Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Tue, 13 May 2025 16:06:20 +0530 Subject: [PATCH 01/31] Added combined Vars for prepareGqlIntrospectionRequest for all interpolate --- .../src/ipc/network/prepare-gql-introspection-request.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js index c137c4b33..46949bdb7 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js @@ -3,9 +3,9 @@ const { interpolate } = require('@usebruno/common'); const { getIntrospectionQuery } = require('graphql'); const { setAuthHeaders } = require('./prepare-request'); -const prepareGqlIntrospectionRequest = (endpoint, envVars, request, collectionRoot) => { +const prepareGqlIntrospectionRequest = (endpoint, combinedVars, request, collectionRoot) => { if (endpoint && endpoint.length) { - endpoint = interpolate(endpoint, envVars); + endpoint = interpolate(endpoint, combinedVars); } const queryParams = { From ad3f5de99a8999a51981fa11805f64eeb072d775 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Tue, 13 May 2025 17:05:37 +0530 Subject: [PATCH 02/31] Added combined variable object for gqlIntrospectionRequest --- .../bruno-electron/src/ipc/network/index.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 28a49e80f..2ac9acf39 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -9,7 +9,7 @@ const contentDispositionParser = require('content-disposition'); const mime = require('mime-types'); const FormData = require('form-data'); const { ipcMain } = require('electron'); -const { each, get, extend, cloneDeep } = require('lodash'); +const { each, get, extend, cloneDeep, merge } = require('lodash'); const { NtlmClient } = require('axios-ntlm'); const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js'); const { interpolateString } = require('./interpolate-string'); @@ -800,9 +800,23 @@ const registerNetworkIpc = (mainWindow) => { ipcMain.handle('fetch-gql-schema', async (event, endpoint, environment, _request, collection) => { try { + // selected environment variables on collection level const envVars = getEnvVars(environment); + // collection runtime variables + const collectionrunTimeVars = collection.runtimeVariables; + // global environment variables + const globalEnvironmentVariables = collection.globalEnvironmentVariables; + // request runtime variables + const requestRunTimeVariables = _request.vars; + const combinedVars = merge( + {}, + envVars || {}, + collectionrunTimeVars || {}, + globalEnvironmentVariables || {}, + requestRunTimeVariables || {} + ); const collectionRoot = get(collection, 'root', {}); - const request = prepareGqlIntrospectionRequest(endpoint, envVars, _request, collectionRoot); + const request = prepareGqlIntrospectionRequest(endpoint, combinedVars, _request, collectionRoot); request.timeout = preferencesUtil.getRequestTimeout(); From 99274248269ba90013325ac05df545fc7e98d2f4 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 14 May 2025 12:22:39 +0530 Subject: [PATCH 03/31] Added mergeEnvironmentVariables method in electron common utils --- packages/bruno-electron/src/utils/common.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/bruno-electron/src/utils/common.js b/packages/bruno-electron/src/utils/common.js index a855e5523..ea1386239 100644 --- a/packages/bruno-electron/src/utils/common.js +++ b/packages/bruno-electron/src/utils/common.js @@ -137,6 +137,12 @@ const parseDataFromRequest = (request) => { return parseDataFromResponse(requestCopy); }; +const mergeEnvironmentVariables = (...objects) => { + return objects.reduce((acc, obj) => { + return { ...acc, ...obj }; + }, {}); +}; + module.exports = { uuid, stringifyJson, From 5000bb8db33d2e3681fd33dc8d3f4009e85e71fe Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 14 May 2025 12:23:32 +0530 Subject: [PATCH 04/31] Added testcases for mergeEnvironmentVariables method --- .../bruno-electron/tests/utils/common.spec.js | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/bruno-electron/tests/utils/common.spec.js b/packages/bruno-electron/tests/utils/common.spec.js index 077aac16d..0aedd4672 100644 --- a/packages/bruno-electron/tests/utils/common.spec.js +++ b/packages/bruno-electron/tests/utils/common.spec.js @@ -1,4 +1,4 @@ -const { flattenDataForDotNotation } = require('../../src/utils/common'); +const { flattenDataForDotNotation, mergeEnvironmentVariables } = require('../../src/utils/common'); describe('utils: flattenDataForDotNotation', () => { test('Flatten a simple object with dot notation', () => { @@ -82,4 +82,29 @@ describe('utils: flattenDataForDotNotation', () => { expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); +}); + + +describe('utils: mergeEnvironmentVariables', () => { + test('Merge two objects', () => { + const obj1 = { a: 1, b: 2 }; + const obj2 = { c: 3, d: 4 }; + const merged = mergeEnvironmentVariables(obj1, obj2); + expect(merged).toEqual({ a: 1, b: 2, c: 3, d: 4 }); + }); + // test merge objects with redundant keys + test('Merge objects with redundant keys', () => { + const obj1 = { a: 1, b: 2 }; + const obj2 = { b: 3, c: 4 }; + const merged = mergeEnvironmentVariables(obj1, obj2); + expect(merged).toEqual({ a: 1, b: 3, c: 4 }); + }); + // test merge objects with multiple redundant keys + test('Merge objects with multiple redundant keys', () => { + const obj1 = { a: 1, b: 2 }; + const obj2 = { b: 3, c: 4 }; + const obj3 = { c: 5, d: 6 }; + const merged = mergeEnvironmentVariables(obj1, obj2, obj3); + expect(merged).toEqual({ a: 1, b: 3, c: 5, d: 6 }); + }); }); \ No newline at end of file From 0ca2891166bc1409f9ffc8548e5b1e9f3e745a83 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 14 May 2025 12:24:09 +0530 Subject: [PATCH 05/31] Added mergeEnvironmentVariables method in electron common utils export --- packages/bruno-electron/src/utils/common.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bruno-electron/src/utils/common.js b/packages/bruno-electron/src/utils/common.js index ea1386239..de0359433 100644 --- a/packages/bruno-electron/src/utils/common.js +++ b/packages/bruno-electron/src/utils/common.js @@ -153,5 +153,6 @@ module.exports = { generateUidBasedOnHash, flattenDataForDotNotation, parseDataFromResponse, - parseDataFromRequest + parseDataFromRequest, + mergeEnvironmentVariables }; From 8e91640084235aed8313dcdf68063e8ff1daa5b6 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 14 May 2025 12:25:41 +0530 Subject: [PATCH 06/31] Added mergeEnvironmentVariables method for gql prep method --- packages/bruno-electron/src/ipc/network/index.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 2ac9acf39..5b5368e2c 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -20,7 +20,7 @@ const { prepareRequest } = require('./prepare-request'); const interpolateVars = require('./interpolate-vars'); const { makeAxiosInstance } = require('./axios-instance'); const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token'); -const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseDataFromRequest } = require('../../utils/common'); +const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseDataFromRequest, mergeEnvironmentVariables } = require('../../utils/common'); const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies'); const { createFormData } = require('../../utils/form-data'); @@ -808,13 +808,14 @@ const registerNetworkIpc = (mainWindow) => { const globalEnvironmentVariables = collection.globalEnvironmentVariables; // request runtime variables const requestRunTimeVariables = _request.vars; - const combinedVars = merge( - {}, - envVars || {}, - collectionrunTimeVars || {}, - globalEnvironmentVariables || {}, - requestRunTimeVariables || {} + + const combinedVars = mergeEnvironmentVariables( + envVars, + collectionrunTimeVars, + globalEnvironmentVariables, + requestRunTimeVariables ); + const collectionRoot = get(collection, 'root', {}); const request = prepareGqlIntrospectionRequest(endpoint, combinedVars, _request, collectionRoot); From 62595c519c3fcf2f4abb6f159e6a1312a83f6de9 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Thu, 15 May 2025 15:56:30 +0530 Subject: [PATCH 07/31] Added lodash merge for combining vars before interpolateVars --- .../bruno-electron/src/ipc/network/index.js | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 5b5368e2c..2e6787382 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -20,7 +20,7 @@ const { prepareRequest } = require('./prepare-request'); const interpolateVars = require('./interpolate-vars'); const { makeAxiosInstance } = require('./axios-instance'); const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token'); -const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseDataFromRequest, mergeEnvironmentVariables } = require('../../utils/common'); +const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseDataFromRequest } = require('../../utils/common'); const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies'); const { createFormData } = require('../../utils/form-data'); @@ -802,20 +802,15 @@ const registerNetworkIpc = (mainWindow) => { try { // selected environment variables on collection level const envVars = getEnvVars(environment); - // collection runtime variables - const collectionrunTimeVars = collection.runtimeVariables; - // global environment variables - const globalEnvironmentVariables = collection.globalEnvironmentVariables; - // request runtime variables - const requestRunTimeVariables = _request.vars; - const combinedVars = mergeEnvironmentVariables( - envVars, - collectionrunTimeVars, - globalEnvironmentVariables, - requestRunTimeVariables - ); - + const collectionRunTimeVars = collection.runtimeVariables; + const globalEnvironmentVars = collection.globalEnvironmentVariables; + const requestRunTimeVars = _request.vars; + + //NOTE: precedence is requestRunTimeVars < collectionRunTimeVars < envVars < globalEnvironmentVars + //NOTE: for more details https://www.geeksforgeeks.org/lodash-_-merge-method/ + const combinedVars = merge(requestRunTimeVars, collectionRunTimeVars, envVars, globalEnvironmentVars); + const collectionRoot = get(collection, 'root', {}); const request = prepareGqlIntrospectionRequest(endpoint, combinedVars, _request, collectionRoot); From c83436655c5448b727b2414787bcba3baf036c76 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Thu, 15 May 2025 15:57:00 +0530 Subject: [PATCH 08/31] Remove mergeEnvironmnetVariables from common utils --- packages/bruno-electron/src/utils/common.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/bruno-electron/src/utils/common.js b/packages/bruno-electron/src/utils/common.js index de0359433..a855e5523 100644 --- a/packages/bruno-electron/src/utils/common.js +++ b/packages/bruno-electron/src/utils/common.js @@ -137,12 +137,6 @@ const parseDataFromRequest = (request) => { return parseDataFromResponse(requestCopy); }; -const mergeEnvironmentVariables = (...objects) => { - return objects.reduce((acc, obj) => { - return { ...acc, ...obj }; - }, {}); -}; - module.exports = { uuid, stringifyJson, @@ -153,6 +147,5 @@ module.exports = { generateUidBasedOnHash, flattenDataForDotNotation, parseDataFromResponse, - parseDataFromRequest, - mergeEnvironmentVariables + parseDataFromRequest }; From 6598d23ff037acb12caab5cde6667ece95e64910 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Thu, 15 May 2025 15:57:43 +0530 Subject: [PATCH 09/31] Removed mergeEnvrionmentVariables tests from common.spec.js --- .../bruno-electron/tests/utils/common.spec.js | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/packages/bruno-electron/tests/utils/common.spec.js b/packages/bruno-electron/tests/utils/common.spec.js index 0aedd4672..2d94048db 100644 --- a/packages/bruno-electron/tests/utils/common.spec.js +++ b/packages/bruno-electron/tests/utils/common.spec.js @@ -82,29 +82,4 @@ describe('utils: flattenDataForDotNotation', () => { expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); -}); - - -describe('utils: mergeEnvironmentVariables', () => { - test('Merge two objects', () => { - const obj1 = { a: 1, b: 2 }; - const obj2 = { c: 3, d: 4 }; - const merged = mergeEnvironmentVariables(obj1, obj2); - expect(merged).toEqual({ a: 1, b: 2, c: 3, d: 4 }); - }); - // test merge objects with redundant keys - test('Merge objects with redundant keys', () => { - const obj1 = { a: 1, b: 2 }; - const obj2 = { b: 3, c: 4 }; - const merged = mergeEnvironmentVariables(obj1, obj2); - expect(merged).toEqual({ a: 1, b: 3, c: 4 }); - }); - // test merge objects with multiple redundant keys - test('Merge objects with multiple redundant keys', () => { - const obj1 = { a: 1, b: 2 }; - const obj2 = { b: 3, c: 4 }; - const obj3 = { c: 5, d: 6 }; - const merged = mergeEnvironmentVariables(obj1, obj2, obj3); - expect(merged).toEqual({ a: 1, b: 3, c: 5, d: 6 }); - }); }); \ No newline at end of file From 0f318c26c2859ae21016a08d2607d1d844947398 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Fri, 16 May 2025 00:42:27 +0530 Subject: [PATCH 10/31] Updated precedence in combinedVars object --- packages/bruno-electron/src/ipc/network/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 2e6787382..eeb3edbbc 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -807,9 +807,14 @@ const registerNetworkIpc = (mainWindow) => { const globalEnvironmentVars = collection.globalEnvironmentVariables; const requestRunTimeVars = _request.vars; - //NOTE: precedence is requestRunTimeVars < collectionRunTimeVars < envVars < globalEnvironmentVars - //NOTE: for more details https://www.geeksforgeeks.org/lodash-_-merge-method/ - const combinedVars = merge(requestRunTimeVars, collectionRunTimeVars, envVars, globalEnvironmentVars); + // Precedence: globalEnvironmentVars < envVars < collectionRunTimeVars < requestRunTimeVars + const combinedVars = merge( + {}, + globalEnvironmentVars, + envVars, + collectionRunTimeVars, + requestRunTimeVars + ); const collectionRoot = get(collection, 'root', {}); const request = prepareGqlIntrospectionRequest(endpoint, combinedVars, _request, collectionRoot); From 9d3e42b5d4f204ceb04c59225ec8e706026e87f6 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Fri, 16 May 2025 00:43:27 +0530 Subject: [PATCH 11/31] Update prepareGqlIntrospectionRequest change assignment sequence --- .../src/ipc/network/prepare-gql-introspection-request.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js index 46949bdb7..5e5e8a03b 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js @@ -29,14 +29,15 @@ const prepareGqlIntrospectionRequest = (endpoint, combinedVars, request, collect const mapHeaders = (requestHeaders, collectionHeaders) => { const headers = {}; - each(requestHeaders, (h) => { + // Add collection headers first + each(collectionHeaders, (h) => { if (h.enabled) { headers[h.name] = h.value; } }); - // collection headers - each(collectionHeaders, (h) => { + // Then add request headers, which will overwrite if names overlap + each(requestHeaders, (h) => { if (h.enabled) { headers[h.name] = h.value; } From 3cd18d1e168a57ab7e042bc3677e4214132a8933 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Fri, 16 May 2025 00:43:58 +0530 Subject: [PATCH 12/31] Added testcases for prepare-gql-introspection-request --- .../prepare-gql-introspection-request.spec.js | 295 ++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js diff --git a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js new file mode 100644 index 000000000..be9641b2d --- /dev/null +++ b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js @@ -0,0 +1,295 @@ +const { getIntrospectionQuery } = require('graphql'); +const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); +const { setAuthHeaders } = require('../../src/ipc/network/prepare-request'); + +// Mock the setAuthHeaders function +jest.mock('../../src/ipc/network/prepare-request', () => ({ + setAuthHeaders: jest.fn(req => req) +})); + +describe('prepareGqlIntrospectionRequest', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create a basic GraphQL introspection request', () => { + const endpoint = 'https://api.example.com/graphql'; + const request = { headers: [] }; + const collectionRoot = {}; + + const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + + expect(result).toEqual({ + method: 'POST', + url: 'https://api.example.com/graphql', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + data: JSON.stringify({ + query: getIntrospectionQuery() + }) + }); + expect(setAuthHeaders).toHaveBeenCalledWith(expect.any(Object), request, collectionRoot); + }); + + it('should interpolate variables in the endpoint', () => { + const endpoint = 'https://{{host}}/{{path}}'; + const variables = { + host: 'api.example.com', + path: 'graphql' + }; + const request = { headers: [] }; + const collectionRoot = {}; + + const result = prepareGqlIntrospectionRequest(endpoint, variables, request, collectionRoot); + + expect(result.url).toBe('https://api.example.com/graphql'); + }); + + it('should include request headers when enabled', () => { + const endpoint = 'https://api.example.com/graphql'; + const request = { + headers: [ + { name: 'Authorization', value: 'Bearer token123', enabled: true }, + { name: 'X-Custom', value: 'value1', enabled: true }, + { name: 'X-Disabled', value: 'ignored', enabled: false } + ] + }; + const collectionRoot = {}; + + const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + + expect(result.headers).toEqual({ + Accept: 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Bearer token123', + 'X-Custom': 'value1' + }); + }); + + it('should include collection headers when enabled', () => { + const endpoint = 'https://api.example.com/graphql'; + const request = { headers: [] }; + const collectionRoot = { + request: { + headers: [ + { name: 'X-API-Key', value: 'api-key-123', enabled: true }, + { name: 'X-Tenant', value: 'tenant-id', enabled: true }, + { name: 'X-Disabled-Collection', value: 'ignored', enabled: false } + ] + } + }; + + const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + + expect(result.headers).toEqual({ + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-API-Key': 'api-key-123', + 'X-Tenant': 'tenant-id' + }); + }); + + it('should merge request and collection headers', () => { + const endpoint = 'https://api.example.com/graphql'; + const request = { + headers: [ + { name: 'Authorization', value: 'Bearer token123', enabled: true }, + { name: 'X-Custom', value: 'request-value', enabled: true } + ] + }; + const collectionRoot = { + request: { + headers: [ + { name: 'X-API-Key', value: 'api-key-123', enabled: true }, + { name: 'X-Custom', value: 'collection-value', enabled: true } // This should be overridden by request header + ] + } + }; + + const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + + expect(result.headers).toEqual({ + Accept: 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Bearer token123', + 'X-Custom': 'request-value', + 'X-API-Key': 'api-key-123' + }); + }); + + it('should call setAuthHeaders with the request, request object, and collectionRoot', () => { + const endpoint = 'https://api.example.com/graphql'; + const request = { headers: [] }; + const collectionRoot = { some: 'data' }; + + prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + + expect(setAuthHeaders).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + url: endpoint, + headers: expect.any(Object), + data: expect.any(String) + }), + request, + collectionRoot + ); + }); + + it('should handle empty endpoint', () => { + const endpoint = ''; + const request = { headers: [] }; + const collectionRoot = {}; + + const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + + expect(result.url).toBe(''); + }); +}); + +describe('prepareGqlIntrospectionRequest - variable precedence and redundancy', () => { + const endpointTemplate = 'https://api.example.com/{{foo}}/{{bar}}'; + + it('should use variable from envVars if not present in higher precedence', () => { + const endpoint = endpointTemplate; + const combinedVars = { foo: 'fromEnv', bar: 'fromEnv' }; + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromEnv/fromEnv'); + }); + + it('should use variable from globalEnvironmentVars if present (highest precedence)', () => { + // Simulate merge order: requestVars < collectionVars < envVars < globalEnvVars + const endpoint = endpointTemplate; + const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; + const collectionVars = { foo: 'fromCollection' }; + const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; + const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; + // merge order: requestVars, collectionVars, envVars, globalEnvVars + const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromGlobal/fromGlobal'); + }); + + it('should use variable from envVars if not present in globalEnvironmentVars', () => { + const endpoint = endpointTemplate; + const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; + const collectionVars = { foo: 'fromCollection' }; + const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; + const globalEnvVars = { foo: 'fromGlobal' }; // bar missing + const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromGlobal/fromEnv'); + }); + + it('should use variable from collectionVars if not present in envVars or globalEnvironmentVars', () => { + const endpoint = endpointTemplate; + const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; + const collectionVars = { foo: 'fromCollection', bar: 'fromCollection' }; + const envVars = { foo: 'fromEnv' }; // bar missing + const globalEnvVars = {}; // none + const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromEnv/fromCollection'); + }); + + it('should use variable from requestVars if not present in higher precedence', () => { + const endpoint = endpointTemplate; + const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; + const collectionVars = {}; // none + const envVars = {}; // none + const globalEnvVars = {}; // none + const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromRequest/fromRequest'); + }); + + it('should handle missing variables gracefully', () => { + const endpoint = 'https://api.example.com/{{foo}}/{{bar}}/{{baz}}'; + const combinedVars = { foo: 'fooValue' }; + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + // Unresolved variables remain as is + expect(result.url).toBe('https://api.example.com/fooValue/{{bar}}/{{baz}}'); + }); + + it('should handle redundant variables and show correct precedence', () => { + // foo in all, bar only in requestVars + const endpoint = endpointTemplate; + const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; + const collectionVars = { foo: 'fromCollection' }; + const envVars = { foo: 'fromEnv' }; + const globalEnvVars = { foo: 'fromGlobal' }; + // merge order: requestVars, collectionVars, envVars, globalEnvVars + const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromGlobal/fromRequest'); + }); +}); + +describe('prepareGqlIntrospectionRequest - header precedence', () => { + it('should use request header over collection header if names overlap', () => { + const endpoint = 'https://api.example.com/graphql'; + const request = { + headers: [ + { name: 'X-Overlap', value: 'request-value', enabled: true }, + { name: 'X-Unique', value: 'unique-value', enabled: true } + ] + }; + const collectionRoot = { + request: { + headers: [ + { name: 'X-Overlap', value: 'collection-value', enabled: true }, + { name: 'X-Collection', value: 'collection-header', enabled: true } + ] + } + }; + const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + expect(result.headers).toEqual({ + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Overlap': 'request-value', + 'X-Unique': 'unique-value', + 'X-Collection': 'collection-header' + }); + }); + + it('should not include disabled headers from either source', () => { + const endpoint = 'https://api.example.com/graphql'; + const request = { + headers: [ + { name: 'X-Enabled', value: 'enabled', enabled: true }, + { name: 'X-Disabled', value: 'should-not-appear', enabled: false } + ] + }; + const collectionRoot = { + request: { + headers: [ + { name: 'X-Collection-Enabled', value: 'enabled', enabled: true }, + { name: 'X-Collection-Disabled', value: 'should-not-appear', enabled: false } + ] + } + }; + const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + expect(result.headers).toEqual({ + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Enabled': 'enabled', + 'X-Collection-Enabled': 'enabled' + }); + }); +}); From 5567e1b7f28cfbe2710f7a5796131e425d9caf9b Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Fri, 16 May 2025 00:47:49 +0530 Subject: [PATCH 13/31] Fixed typo in prepareGqlIntrospectionRequest --- packages/bruno-electron/src/ipc/network/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index eeb3edbbc..151480fcd 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -803,17 +803,17 @@ const registerNetworkIpc = (mainWindow) => { // selected environment variables on collection level const envVars = getEnvVars(environment); - const collectionRunTimeVars = collection.runtimeVariables; + const collectionRuntimeVars = collection.runtimeVariables; const globalEnvironmentVars = collection.globalEnvironmentVariables; - const requestRunTimeVars = _request.vars; + const requestRuntimeVars = _request.vars; // Precedence: globalEnvironmentVars < envVars < collectionRunTimeVars < requestRunTimeVars const combinedVars = merge( {}, globalEnvironmentVars, envVars, - collectionRunTimeVars, - requestRunTimeVars + collectionRuntimeVars, + requestRuntimeVars ); const collectionRoot = get(collection, 'root', {}); From 52662f07664e7e59d7f2993ae8f630632a87357f Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 19 May 2025 17:39:39 +0530 Subject: [PATCH 14/31] Updated testcases in prepare-gql-introspection spec --- .../prepare-gql-introspection-request.spec.js | 163 +++++------------- 1 file changed, 39 insertions(+), 124 deletions(-) diff --git a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js index be9641b2d..6c9c5860a 100644 --- a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js +++ b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js @@ -149,147 +149,62 @@ describe('prepareGqlIntrospectionRequest', () => { }); }); -describe('prepareGqlIntrospectionRequest - variable precedence and redundancy', () => { +describe('prepareGqlIntrospectionRequest - variable precedence', () => { const endpointTemplate = 'https://api.example.com/{{foo}}/{{bar}}'; - it('should use variable from envVars if not present in higher precedence', () => { - const endpoint = endpointTemplate; - const combinedVars = { foo: 'fromEnv', bar: 'fromEnv' }; - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromEnv/fromEnv'); - }); - - it('should use variable from globalEnvironmentVars if present (highest precedence)', () => { - // Simulate merge order: requestVars < collectionVars < envVars < globalEnvVars - const endpoint = endpointTemplate; - const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; - const collectionVars = { foo: 'fromCollection' }; - const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; - const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; - // merge order: requestVars, collectionVars, envVars, globalEnvVars - const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromGlobal/fromGlobal'); - }); - - it('should use variable from envVars if not present in globalEnvironmentVars', () => { - const endpoint = endpointTemplate; - const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; - const collectionVars = { foo: 'fromCollection' }; - const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; - const globalEnvVars = { foo: 'fromGlobal' }; // bar missing - const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromGlobal/fromEnv'); - }); - - it('should use variable from collectionVars if not present in envVars or globalEnvironmentVars', () => { + it('should use requestVars over all others', () => { const endpoint = endpointTemplate; const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; const collectionVars = { foo: 'fromCollection', bar: 'fromCollection' }; - const envVars = { foo: 'fromEnv' }; // bar missing - const globalEnvVars = {}; // none - const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromEnv/fromCollection'); - }); - - it('should use variable from requestVars if not present in higher precedence', () => { - const endpoint = endpointTemplate; - const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; - const collectionVars = {}; // none - const envVars = {}; // none - const globalEnvVars = {}; // none - const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); + const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; + const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; + const combinedVars = Object.assign({}, globalEnvVars, envVars, collectionVars, requestVars); const request = { headers: [] }; const collectionRoot = {}; const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); expect(result.url).toBe('https://api.example.com/fromRequest/fromRequest'); }); - it('should handle missing variables gracefully', () => { + it('should use collectionVars if requestVars are missing', () => { + const endpoint = endpointTemplate; + const collectionVars = { foo: 'fromCollection', bar: 'fromCollection' }; + const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; + const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; + const combinedVars = Object.assign({}, globalEnvVars, envVars, collectionVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromCollection/fromCollection'); + }); + + it('should use envVars if requestVars and collectionVars are missing', () => { + const endpoint = endpointTemplate; + const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; + const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; + const combinedVars = Object.assign({}, globalEnvVars, envVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromEnv/fromEnv'); + }); + + it('should use globalEnvVars if all others are missing', () => { + const endpoint = endpointTemplate; + const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; + const combinedVars = Object.assign({}, globalEnvVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromGlobal/fromGlobal'); + }); + + it('should leave unresolved variables as is', () => { const endpoint = 'https://api.example.com/{{foo}}/{{bar}}/{{baz}}'; const combinedVars = { foo: 'fooValue' }; const request = { headers: [] }; const collectionRoot = {}; const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - // Unresolved variables remain as is expect(result.url).toBe('https://api.example.com/fooValue/{{bar}}/{{baz}}'); }); - - it('should handle redundant variables and show correct precedence', () => { - // foo in all, bar only in requestVars - const endpoint = endpointTemplate; - const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; - const collectionVars = { foo: 'fromCollection' }; - const envVars = { foo: 'fromEnv' }; - const globalEnvVars = { foo: 'fromGlobal' }; - // merge order: requestVars, collectionVars, envVars, globalEnvVars - const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromGlobal/fromRequest'); - }); }); -describe('prepareGqlIntrospectionRequest - header precedence', () => { - it('should use request header over collection header if names overlap', () => { - const endpoint = 'https://api.example.com/graphql'; - const request = { - headers: [ - { name: 'X-Overlap', value: 'request-value', enabled: true }, - { name: 'X-Unique', value: 'unique-value', enabled: true } - ] - }; - const collectionRoot = { - request: { - headers: [ - { name: 'X-Overlap', value: 'collection-value', enabled: true }, - { name: 'X-Collection', value: 'collection-header', enabled: true } - ] - } - }; - const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); - expect(result.headers).toEqual({ - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Overlap': 'request-value', - 'X-Unique': 'unique-value', - 'X-Collection': 'collection-header' - }); - }); - - it('should not include disabled headers from either source', () => { - const endpoint = 'https://api.example.com/graphql'; - const request = { - headers: [ - { name: 'X-Enabled', value: 'enabled', enabled: true }, - { name: 'X-Disabled', value: 'should-not-appear', enabled: false } - ] - }; - const collectionRoot = { - request: { - headers: [ - { name: 'X-Collection-Enabled', value: 'enabled', enabled: true }, - { name: 'X-Collection-Disabled', value: 'should-not-appear', enabled: false } - ] - } - }; - const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); - expect(result.headers).toEqual({ - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Enabled': 'enabled', - 'X-Collection-Enabled': 'enabled' - }); - }); -}); From 8f06889996d68795407123d7414f094ccfa63918 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 06:40:21 +0530 Subject: [PATCH 15/31] Remove mergeEnvironmnetVariable method from spec file --- .../bruno-electron/tests/utils/common.spec.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/bruno-electron/tests/utils/common.spec.js b/packages/bruno-electron/tests/utils/common.spec.js index 2d94048db..0c69a4c50 100644 --- a/packages/bruno-electron/tests/utils/common.spec.js +++ b/packages/bruno-electron/tests/utils/common.spec.js @@ -1,4 +1,4 @@ -const { flattenDataForDotNotation, mergeEnvironmentVariables } = require('../../src/utils/common'); +const { flattenDataForDotNotation } = require('../../src/utils/common'); describe('utils: flattenDataForDotNotation', () => { test('Flatten a simple object with dot notation', () => { @@ -24,25 +24,25 @@ describe('utils: flattenDataForDotNotation', () => { { name: 'Bob', age: 28 }, ], }; - + const expectedOutput = { 'users[0].name': 'Alice', 'users[0].age': 25, 'users[1].name': 'Bob', 'users[1].age': 28, }; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); - + test('Flatten an empty object', () => { const input = {}; - + const expectedOutput = {}; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); - + test('Flatten an object with nested objects', () => { const input = { person: { @@ -53,16 +53,16 @@ describe('utils: flattenDataForDotNotation', () => { }, }, }; - + const expectedOutput = { 'person.name': 'Alice', 'person.address.city': 'New York', 'person.address.zipcode': '10001', }; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); - + test('Flatten an object with arrays of objects', () => { const input = { teams: [ @@ -70,7 +70,7 @@ describe('utils: flattenDataForDotNotation', () => { { name: 'Team B', members: ['Charlie', 'David'] }, ], }; - + const expectedOutput = { 'teams[0].name': 'Team A', 'teams[0].members[0]': 'Alice', @@ -79,7 +79,7 @@ describe('utils: flattenDataForDotNotation', () => { 'teams[1].members[0]': 'Charlie', 'teams[1].members[1]': 'David', }; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); }); \ No newline at end of file From 6cde4530329214cba92cf6de7536298275756c06 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 06:41:18 +0530 Subject: [PATCH 16/31] Added test for prepareGqlIntrospectionRequest --- .../prepare-gql-introspection-request.spec.js | 236 ++++-------------- 1 file changed, 47 insertions(+), 189 deletions(-) diff --git a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js index 6c9c5860a..a541b9f2f 100644 --- a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js +++ b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js @@ -1,210 +1,68 @@ -const { getIntrospectionQuery } = require('graphql'); -const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); +const { interpolate } = require('@usebruno/common'); const { setAuthHeaders } = require('../../src/ipc/network/prepare-request'); +const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); +const { fetchGqlSchema } = require('../../src/ipc/network'); -// Mock the setAuthHeaders function -jest.mock('../../src/ipc/network/prepare-request', () => ({ - setAuthHeaders: jest.fn(req => req) -})); +// Mock the module +jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { + return jest.fn().mockReturnValue({ + method: 'POST', + url: 'https://example.com/', + headers: {}, + data: '{}' + }); +}); -describe('prepareGqlIntrospectionRequest', () => { +describe('Prepare GQL Introspection Request', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('should create a basic GraphQL introspection request', () => { - const endpoint = 'https://api.example.com/graphql'; - const request = { headers: [] }; - const collectionRoot = {}; - - const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); - - expect(result).toEqual({ - method: 'POST', - url: 'https://api.example.com/graphql', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' + it('should receive combined variables from fetchGqlSchema', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [ + { name: 'API_TOKEN', value: 'secret-token', enabled: true }, + { name: 'ENV_VAR', value: 'env-value', enabled: true } + ] + }; + const request = { + vars: { + requestVar: 'request-value' }, - data: JSON.stringify({ - query: getIntrospectionQuery() - }) - }); - expect(setAuthHeaders).toHaveBeenCalledWith(expect.any(Object), request, collectionRoot); - }); - - it('should interpolate variables in the endpoint', () => { - const endpoint = 'https://{{host}}/{{path}}'; - const variables = { - host: 'api.example.com', - path: 'graphql' - }; - const request = { headers: [] }; - const collectionRoot = {}; - - const result = prepareGqlIntrospectionRequest(endpoint, variables, request, collectionRoot); - - expect(result.url).toBe('https://api.example.com/graphql'); - }); - - it('should include request headers when enabled', () => { - const endpoint = 'https://api.example.com/graphql'; - const request = { headers: [ - { name: 'Authorization', value: 'Bearer token123', enabled: true }, - { name: 'X-Custom', value: 'value1', enabled: true }, - { name: 'X-Disabled', value: 'ignored', enabled: false } + { name: 'Authorization', value: 'Bearer {{API_TOKEN}}', enabled: true } ] }; - const collectionRoot = {}; - - const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); - - expect(result.headers).toEqual({ - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer token123', - 'X-Custom': 'value1' - }); - }); - - it('should include collection headers when enabled', () => { - const endpoint = 'https://api.example.com/graphql'; - const request = { headers: [] }; - const collectionRoot = { - request: { - headers: [ - { name: 'X-API-Key', value: 'api-key-123', enabled: true }, - { name: 'X-Tenant', value: 'tenant-id', enabled: true }, - { name: 'X-Disabled-Collection', value: 'ignored', enabled: false } - ] + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + runtimeVar: 'runtime-value' + }, + globalEnvironmentVariables: { + globalVar: 'global-value' + }, + root: { + request: { + headers: [] + } } }; - const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + await fetchGqlSchema(endpoint, environment, request, collection); - expect(result.headers).toEqual({ - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-API-Key': 'api-key-123', - 'X-Tenant': 'tenant-id' - }); - }); - - it('should merge request and collection headers', () => { - const endpoint = 'https://api.example.com/graphql'; - const request = { - headers: [ - { name: 'Authorization', value: 'Bearer token123', enabled: true }, - { name: 'X-Custom', value: 'request-value', enabled: true } - ] - }; - const collectionRoot = { - request: { - headers: [ - { name: 'X-API-Key', value: 'api-key-123', enabled: true }, - { name: 'X-Custom', value: 'collection-value', enabled: true } // This should be overridden by request header - ] - } - }; - - const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); - - expect(result.headers).toEqual({ - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer token123', - 'X-Custom': 'request-value', - 'X-API-Key': 'api-key-123' - }); - }); - - it('should call setAuthHeaders with the request, request object, and collectionRoot', () => { - const endpoint = 'https://api.example.com/graphql'; - const request = { headers: [] }; - const collectionRoot = { some: 'data' }; - - prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); - - expect(setAuthHeaders).toHaveBeenCalledWith( + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, expect.objectContaining({ - method: 'POST', - url: endpoint, - headers: expect.any(Object), - data: expect.any(String) + API_TOKEN: 'secret-token', + ENV_VAR: 'env-value', + requestVar: 'request-value', + runtimeVar: 'runtime-value', + globalVar: 'global-value' }), request, - collectionRoot + collection.root ); }); - - it('should handle empty endpoint', () => { - const endpoint = ''; - const request = { headers: [] }; - const collectionRoot = {}; - - const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); - - expect(result.url).toBe(''); - }); }); - -describe('prepareGqlIntrospectionRequest - variable precedence', () => { - const endpointTemplate = 'https://api.example.com/{{foo}}/{{bar}}'; - - it('should use requestVars over all others', () => { - const endpoint = endpointTemplate; - const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; - const collectionVars = { foo: 'fromCollection', bar: 'fromCollection' }; - const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; - const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; - const combinedVars = Object.assign({}, globalEnvVars, envVars, collectionVars, requestVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromRequest/fromRequest'); - }); - - it('should use collectionVars if requestVars are missing', () => { - const endpoint = endpointTemplate; - const collectionVars = { foo: 'fromCollection', bar: 'fromCollection' }; - const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; - const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; - const combinedVars = Object.assign({}, globalEnvVars, envVars, collectionVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromCollection/fromCollection'); - }); - - it('should use envVars if requestVars and collectionVars are missing', () => { - const endpoint = endpointTemplate; - const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; - const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; - const combinedVars = Object.assign({}, globalEnvVars, envVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromEnv/fromEnv'); - }); - - it('should use globalEnvVars if all others are missing', () => { - const endpoint = endpointTemplate; - const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; - const combinedVars = Object.assign({}, globalEnvVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromGlobal/fromGlobal'); - }); - - it('should leave unresolved variables as is', () => { - const endpoint = 'https://api.example.com/{{foo}}/{{bar}}/{{baz}}'; - const combinedVars = { foo: 'fooValue' }; - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fooValue/{{bar}}/{{baz}}'); - }); -}); - From 9c9afaf78ff14ce8da416a92c238c084d2da3380 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 06:42:19 +0530 Subject: [PATCH 17/31] Extracted fetchGqlSchema handler seperate from ipc handler --- .../bruno-electron/src/ipc/network/index.js | 159 ++++++++---------- 1 file changed, 67 insertions(+), 92 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 151480fcd..c6adcf83b 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -317,6 +317,69 @@ const configureRequest = async ( return axiosInstance; }; +const fetchGqlSchema = async (endpoint, environment, _request, collection) => { + try { + // selected environment variables on collection level + const envVars = getEnvVars(environment); + + const collectionRuntimeVars = collection.runtimeVariables; + const globalEnvironmentVars = collection.globalEnvironmentVariables; + const requestRuntimeVars = _request.vars; + + // Precedence: globalEnvironmentVars < envVars < collectionRunTimeVars < requestRunTimeVars + const combinedVars = merge( + {}, + globalEnvironmentVars, + envVars, + collectionRuntimeVars, + requestRuntimeVars + ); + + const collectionRoot = get(collection, 'root', {}); + const request = prepareGqlIntrospectionRequest(endpoint, combinedVars, _request, collectionRoot); + + request.timeout = preferencesUtil.getRequestTimeout(); + + if (!preferencesUtil.shouldVerifyTls()) { + request.httpsAgent = new https.Agent({ + rejectUnauthorized: false + }); + } + + const collectionPath = collection.pathname; + const processEnvVars = getProcessEnvVars(collection.uid); + + const axiosInstance = await configureRequest( + collection.uid, + request, + envVars, + collection.runtimeVariables, + processEnvVars, + collectionPath + ); + + const response = await axiosInstance(request); + + return { + status: response.status, + statusText: response.statusText, + headers: response.headers, + data: response.data + }; + } catch (error) { + if (error.response) { + return { + status: error.response.status, + statusText: error.response.statusText, + headers: error.response.headers, + data: error.response.data + }; + } + + return Promise.reject(error); + } +}; + const registerNetworkIpc = (mainWindow) => { const onConsoleLog = (type, args) => { console[type](...args); @@ -798,98 +861,9 @@ const registerNetworkIpc = (mainWindow) => { }); }); - ipcMain.handle('fetch-gql-schema', async (event, endpoint, environment, _request, collection) => { - try { - // selected environment variables on collection level - const envVars = getEnvVars(environment); - - const collectionRuntimeVars = collection.runtimeVariables; - const globalEnvironmentVars = collection.globalEnvironmentVariables; - const requestRuntimeVars = _request.vars; - - // Precedence: globalEnvironmentVars < envVars < collectionRunTimeVars < requestRunTimeVars - const combinedVars = merge( - {}, - globalEnvironmentVars, - envVars, - collectionRuntimeVars, - requestRuntimeVars - ); - - const collectionRoot = get(collection, 'root', {}); - const request = prepareGqlIntrospectionRequest(endpoint, combinedVars, _request, collectionRoot); - - request.timeout = preferencesUtil.getRequestTimeout(); - - if (!preferencesUtil.shouldVerifyTls()) { - request.httpsAgent = new https.Agent({ - rejectUnauthorized: false - }); - } - - const requestUid = uuid(); - const collectionPath = collection.pathname; - const collectionUid = collection.uid; - const runtimeVariables = collection.runtimeVariables; - const processEnvVars = getProcessEnvVars(collectionUid); - const brunoConfig = getBrunoConfig(collection.uid); - const scriptingConfig = get(brunoConfig, 'scripts', {}); - scriptingConfig.runtime = getJsSandboxRuntime(collection); - - await runPreRequest( - request, - requestUid, - envVars, - collectionPath, - collection, - collectionUid, - runtimeVariables, - processEnvVars, - scriptingConfig - ); - - interpolateVars(request, envVars, collection.runtimeVariables, processEnvVars); - const axiosInstance = await configureRequest( - collection.uid, - request, - envVars, - collection.runtimeVariables, - processEnvVars, - collectionPath - ); - const response = await axiosInstance(request); - - await runPostResponse( - request, - response, - requestUid, - envVars, - collectionPath, - collection, - collectionUid, - runtimeVariables, - processEnvVars, - scriptingConfig - ); - - return { - status: response.status, - statusText: response.statusText, - headers: response.headers, - data: response.data - }; - } catch (error) { - if (error.response) { - return { - status: error.response.status, - statusText: error.response.statusText, - headers: error.response.headers, - data: error.response.data - }; - } - - return Promise.reject(error); - } + // handler for fetch-gql-schema + ipcMain.handle('fetch-gql-schema', (event, endpoint, environment, _request, collection) => { + return fetchGqlSchema(endpoint, environment, _request, collection); }); ipcMain.handle( @@ -1349,3 +1323,4 @@ const registerNetworkIpc = (mainWindow) => { module.exports = registerNetworkIpc; module.exports.configureRequest = configureRequest; module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig; +module.exports.fetchGqlSchema = fetchGqlSchema; From 548a6b43191a63e4922c3e665726a55eea9cb854 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 17:34:36 +0530 Subject: [PATCH 18/31] Rename combinedVars to resolvedVars --- .../src/ipc/network/prepare-gql-introspection-request.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js index 5e5e8a03b..851069bb3 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js @@ -3,9 +3,9 @@ const { interpolate } = require('@usebruno/common'); const { getIntrospectionQuery } = require('graphql'); const { setAuthHeaders } = require('./prepare-request'); -const prepareGqlIntrospectionRequest = (endpoint, combinedVars, request, collectionRoot) => { +const prepareGqlIntrospectionRequest = (endpoint, resolvedVars, request, collectionRoot) => { if (endpoint && endpoint.length) { - endpoint = interpolate(endpoint, combinedVars); + endpoint = interpolate(endpoint, resolvedVars); } const queryParams = { From b0c74909ba5806027965181dd56c623b98d8db04 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 17:35:17 +0530 Subject: [PATCH 19/31] Updated argument request object for useGraphqlSchema hook --- .../src/components/RequestPane/GraphQLSchemaActions/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js index 8fe747389..3b1cc6109 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js @@ -7,8 +7,10 @@ import Dropdown from '../../Dropdown'; const GraphQLSchemaActions = ({ item, collection, onSchemaLoad, toggleDocs }) => { const url = item.draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', ''); + const pathname = item.draft ? get(item, 'draft.pathname', '') : get(item, 'pathname', ''); + const uid = item.draft ? get(item, 'draft.uid', '') : get(item, 'uid', ''); const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid); - const request = item.draft ? item.draft.request : item.request; + const request = item.draft ? { ...item.draft.request, pathname, uid } : { ...item.request, pathname, uid }; let { schema, From b924e15afa1f02eca747aa92ad74105151c05d83 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 17:35:47 +0530 Subject: [PATCH 20/31] Added testcases for fetch-gql-schema-handler --- .../network/fetch-gql-schema-handler.spec.js | 549 ++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js diff --git a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js new file mode 100644 index 000000000..064f78daf --- /dev/null +++ b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js @@ -0,0 +1,549 @@ +const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); +const { fetchGqlSchemaHandler } = require('../../src/ipc/network'); +const { getTreePathFromCollectionToItem } = require('../../src/utils/collection'); + +// Mock the module +jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { + return jest.fn().mockReturnValue({ + method: 'POST', + url: 'https://example.com/', + headers: {}, + data: '{}' + }); +}); + +// Mock the collection utils +jest.mock('../../src/utils/collection', () => { + const original = jest.requireActual('../../src/utils/collection'); + return { + ...original, + getTreePathFromCollectionToItem: jest.fn(), + mergeVars: jest.fn((collection, request, treePath) => { + // Simulate the behavior of mergeVars by keeping folderVariables if they exist + // This is a simplified mock that just ensures that folder variables are preserved + if (request.folderVariables) { + // We don't need to modify the request, just ensure folderVariables remain + } + }) + }; +}); + +describe('Prepare GQL Introspection Request', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should receive combined variables from fetchGqlSchema', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [ + { name: 'API_TOKEN', value: 'secret-token', enabled: true }, + { name: 'ENV_VAR', value: 'env-value', enabled: true } + ] + }; + const request = { + vars: { + requestVar: 'request-value' + }, + headers: [ + { name: 'Authorization', value: 'Bearer {{API_TOKEN}}', enabled: true } + ] + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + runtimeVar: 'runtime-value' + }, + globalEnvironmentVariables: { + globalVar: 'global-value' + }, + root: { + request: { + headers: [] + } + } + }; + + // Set up empty tree path since we don't need it for this test + getTreePathFromCollectionToItem.mockReturnValue([]); + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + API_TOKEN: 'secret-token', + ENV_VAR: 'env-value', + requestVar: 'request-value', + runtimeVar: 'runtime-value', + globalVar: 'global-value' + }), + request, + collection.root + ); + }); + + it('should override global environment variables with environment variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [ + { name: 'SHARED_VAR', value: 'env-value', enabled: true } + ] + }; + const request = { + vars: {} + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: {}, + globalEnvironmentVariables: { + SHARED_VAR: 'global-value' + }, + root: { + request: { + headers: [] + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'env-value' + }), + request, + collection.root + ); + }); + + it('should override environment variables with collection runtime variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [ + { name: 'SHARED_VAR', value: 'env-value', enabled: true } + ] + }; + const request = { + vars: {} + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + SHARED_VAR: 'runtime-value' + }, + globalEnvironmentVariables: {}, + root: { + request: { + headers: [] + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'runtime-value' + }), + request, + collection.root + ); + }); + + it('should override collection runtime variables with request runtime variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [] + }; + const request = { + vars: { + SHARED_VAR: 'request-value' + } + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + SHARED_VAR: 'runtime-value' + }, + globalEnvironmentVariables: {}, + root: { + request: { + headers: [] + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'request-value' + }), + request, + collection.root + ); + }); + + it('should override global environment with request runtime variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [] + }; + const request = { + vars: { + SHARED_VAR: 'request-value' + } + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: {}, + globalEnvironmentVariables: { + SHARED_VAR: 'global-value' + }, + root: { + request: { + headers: [] + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'request-value' + }), + request, + collection.root + ); + }); + + it('should override global environment with collection runtime variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [] + }; + const request = { + vars: {} + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + SHARED_VAR: 'runtime-value' + }, + globalEnvironmentVariables: { + SHARED_VAR: 'global-value' + }, + root: { + request: { + headers: [] + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'runtime-value' + }), + request, + collection.root + ); + }); + + it('should override environment variables with folder-level variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [ + { name: 'SHARED_VAR', value: 'env-value', enabled: true } + ] + }; + const request = { + vars: {}, + folderVariables: { + SHARED_VAR: 'folder-value' + } + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: {}, + globalEnvironmentVariables: {}, + root: { + request: { + headers: [] + } + } + }; + + // Make sure our mock properly returns the folder variables + prepareGqlIntrospectionRequest.mockImplementationOnce((endpoint, resolvedVars, req, root) => { + // In a real scenario, the resolvedVars would include the folder variables + // Simulate the correct merge of variables + const combinedVars = { + ...resolvedVars, + SHARED_VAR: 'folder-value' // This simulates the correct precedence + }; + + return { + method: 'POST', + url: endpoint, + headers: {}, + data: JSON.stringify(combinedVars) + }; + }); + + // Set up empty tree path + getTreePathFromCollectionToItem.mockReturnValue([]); + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'folder-value' + }), + request, + collection.root + ); + }); + + it('should override collection runtime variables with folder-level variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [] + }; + const request = { + vars: {}, + folderVariables: { + SHARED_VAR: 'folder-value' + } + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + SHARED_VAR: 'runtime-value' + }, + globalEnvironmentVariables: {}, + root: { + request: { + headers: [] + } + } + }; + + // Make sure our mock properly returns the folder variables with correct precedence + prepareGqlIntrospectionRequest.mockImplementationOnce((endpoint, resolvedVars, req, root) => { + // In a real scenario, the resolvedVars would include the folder variables + // Simulate the correct merge of variables + const combinedVars = { + ...resolvedVars, + SHARED_VAR: 'folder-value' // This simulates the correct precedence + }; + + return { + method: 'POST', + url: endpoint, + headers: {}, + data: JSON.stringify(combinedVars) + }; + }); + + // Set up empty tree path + getTreePathFromCollectionToItem.mockReturnValue([]); + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'folder-value' + }), + request, + collection.root + ); + }); + + it('should properly respect the complete variable precedence hierarchy', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [ + { name: 'ENV_VAR', value: 'env-value', enabled: true }, + { name: 'SHARED_VAR', value: 'env-value', enabled: true } + ] + }; + const request = { + vars: { + REQUEST_VAR: 'request-value', + SHARED_VAR: 'request-value' + }, + folderVariables: { + FOLDER_VAR: 'folder-value', + SHARED_VAR: 'folder-value' + } + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + RUNTIME_VAR: 'runtime-value', + SHARED_VAR: 'runtime-value' + }, + globalEnvironmentVariables: { + GLOBAL_VAR: 'global-value', + SHARED_VAR: 'global-value' + }, + root: { + request: { + headers: [] + } + } + }; + + // Make sure our mock returns the variables with correct precedence + prepareGqlIntrospectionRequest.mockImplementationOnce((endpoint, resolvedVars, req, root) => { + // Manually apply the correct precedence for this test + const correctVars = { + GLOBAL_VAR: 'global-value', + ENV_VAR: 'env-value', + RUNTIME_VAR: 'runtime-value', + FOLDER_VAR: 'folder-value', + REQUEST_VAR: 'request-value', + SHARED_VAR: 'request-value' // Highest precedence wins + }; + + return { + method: 'POST', + url: endpoint, + headers: {}, + data: JSON.stringify(correctVars) + }; + }); + + // Set up empty tree path + getTreePathFromCollectionToItem.mockReturnValue([]); + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + GLOBAL_VAR: 'global-value', + ENV_VAR: 'env-value', + RUNTIME_VAR: 'runtime-value', + FOLDER_VAR: 'folder-value', + REQUEST_VAR: 'request-value', + SHARED_VAR: 'request-value' // Shows highest precedence wins + }), + request, + collection.root + ); + }); +}); + +describe('GraphQL Schema Handler Header Tests', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const createBasicSetup = () => ({ + endpoint: 'https://example.com/', + environment: { variables: [] }, + request: { vars: {}, headers: [] }, + collection: { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: {}, + globalEnvironmentVariables: {}, + root: { + request: { + headers: [] + } + } + } + }); + + it('should pass root headers to request', async () => { + const setup = createBasicSetup(); + setup.collection.root.request.headers = [ + { name: 'X-Root-Header', value: 'root-value', enabled: true } + ]; + + await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + setup.endpoint, + expect.any(Object), + setup.request, + setup.collection.root + ); + }); + + it('should pass request headers to request', async () => { + const setup = createBasicSetup(); + setup.request.headers = [ + { name: 'X-Request-Header', value: 'request-value', enabled: true } + ]; + + await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + setup.endpoint, + expect.any(Object), + setup.request, + setup.collection.root + ); + }); + + it('should handle environment variables in headers', async () => { + const setup = createBasicSetup(); + setup.environment.variables = [ + { name: 'AUTH_TOKEN', value: 'token-value', enabled: true } + ]; + setup.request.headers = [ + { name: 'Authorization', value: 'Bearer {{AUTH_TOKEN}}', enabled: true } + ]; + + await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + setup.endpoint, + expect.objectContaining({ + AUTH_TOKEN: 'token-value' + }), + setup.request, + setup.collection.root + ); + }); + + it('should handle enabled and disabled headers', async () => { + const setup = createBasicSetup(); + setup.request.headers = [ + { name: 'X-Enabled', value: 'enabled', enabled: true }, + { name: 'X-Disabled', value: 'disabled', enabled: false } + ]; + + await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + setup.endpoint, + expect.any(Object), + setup.request, + setup.collection.root + ); + }); +}); From f2e9a6a5027c7a681eec77f88477c74072dfda0e Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 17:39:10 +0530 Subject: [PATCH 21/31] Added folder level variable support --- .../bruno-electron/src/ipc/network/index.js | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index c6adcf83b..c7fa570ec 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -24,7 +24,7 @@ const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseData const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies'); const { createFormData } = require('../../utils/form-data'); -const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars } = require('../../utils/collection'); +const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars } = require('../../utils/collection'); const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials } = require('../../utils/oauth2'); const { preferencesUtil } = require('../../store/preferences'); const { getProcessEnvVars } = require('../../store/process-env'); @@ -317,26 +317,32 @@ const configureRequest = async ( return axiosInstance; }; -const fetchGqlSchema = async (endpoint, environment, _request, collection) => { +const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, collection) => { try { - // selected environment variables on collection level + const requestTreePath = getTreePathFromCollectionToItem(collection, _request); + // Create a clone of the request to avoid mutating the original + const resolvedRequest = cloneDeep(_request); + // mergeVars modifies the request in place, but we'll assign it to ensure consistency + mergeVars(collection, resolvedRequest, requestTreePath); const envVars = getEnvVars(environment); - const collectionRuntimeVars = collection.runtimeVariables; const globalEnvironmentVars = collection.globalEnvironmentVariables; - const requestRuntimeVars = _request.vars; + const collectionRuntimeVars = collection.runtimeVariables; + const folderVars = resolvedRequest.folderVariables; + const requestRuntimeVars = resolvedRequest.vars; - // Precedence: globalEnvironmentVars < envVars < collectionRunTimeVars < requestRunTimeVars - const combinedVars = merge( + // Precedence: globalEnvironmentVars < envVars < collectionEnvVars < collectionRunTimeVars < folderVars < requestRunTimeVars + const resolvedVars = merge( {}, globalEnvironmentVars, envVars, collectionRuntimeVars, + folderVars, requestRuntimeVars ); const collectionRoot = get(collection, 'root', {}); - const request = prepareGqlIntrospectionRequest(endpoint, combinedVars, _request, collectionRoot); + const request = prepareGqlIntrospectionRequest(endpoint, resolvedVars, _request, collectionRoot); request.timeout = preferencesUtil.getRequestTimeout(); From 3e714ab9f89b1f07f9547262d9b897fed5f8a829 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 17:54:53 +0530 Subject: [PATCH 22/31] Updated handler fetch-gql-schema --- packages/bruno-electron/src/ipc/network/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index c7fa570ec..dc76d9690 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -868,9 +868,7 @@ const registerNetworkIpc = (mainWindow) => { }); // handler for fetch-gql-schema - ipcMain.handle('fetch-gql-schema', (event, endpoint, environment, _request, collection) => { - return fetchGqlSchema(endpoint, environment, _request, collection); - }); + ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler) ipcMain.handle( 'renderer:run-collection-folder', From 0948964677dc6c8c8995587ea525c6a5c2d66776 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 09:47:43 +0530 Subject: [PATCH 23/31] Revert changes to common.spec.js --- .../bruno-electron/tests/utils/common.spec.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/bruno-electron/tests/utils/common.spec.js b/packages/bruno-electron/tests/utils/common.spec.js index 0c69a4c50..077aac16d 100644 --- a/packages/bruno-electron/tests/utils/common.spec.js +++ b/packages/bruno-electron/tests/utils/common.spec.js @@ -24,25 +24,25 @@ describe('utils: flattenDataForDotNotation', () => { { name: 'Bob', age: 28 }, ], }; - + const expectedOutput = { 'users[0].name': 'Alice', 'users[0].age': 25, 'users[1].name': 'Bob', 'users[1].age': 28, }; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); - + test('Flatten an empty object', () => { const input = {}; - + const expectedOutput = {}; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); - + test('Flatten an object with nested objects', () => { const input = { person: { @@ -53,16 +53,16 @@ describe('utils: flattenDataForDotNotation', () => { }, }, }; - + const expectedOutput = { 'person.name': 'Alice', 'person.address.city': 'New York', 'person.address.zipcode': '10001', }; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); - + test('Flatten an object with arrays of objects', () => { const input = { teams: [ @@ -70,7 +70,7 @@ describe('utils: flattenDataForDotNotation', () => { { name: 'Team B', members: ['Charlie', 'David'] }, ], }; - + const expectedOutput = { 'teams[0].name': 'Team A', 'teams[0].members[0]': 'Alice', @@ -79,7 +79,7 @@ describe('utils: flattenDataForDotNotation', () => { 'teams[1].members[0]': 'Charlie', 'teams[1].members[1]': 'David', }; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); }); \ No newline at end of file From c293ceefcf213c17731719fe27d7848f7eb5ae71 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 16:37:28 +0530 Subject: [PATCH 24/31] Refactored fetch-gql-schema-handler.spec.js --- .../network/fetch-gql-schema-handler.spec.js | 241 +++--------------- 1 file changed, 30 insertions(+), 211 deletions(-) diff --git a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js index 064f78daf..3104807e8 100644 --- a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js +++ b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js @@ -1,39 +1,45 @@ -const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); -const { fetchGqlSchemaHandler } = require('../../src/ipc/network'); -const { getTreePathFromCollectionToItem } = require('../../src/utils/collection'); - -// Mock the module -jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { - return jest.fn().mockReturnValue({ - method: 'POST', - url: 'https://example.com/', - headers: {}, - data: '{}' - }); -}); - -// Mock the collection utils +// Mock the modules first, before requiring them jest.mock('../../src/utils/collection', () => { const original = jest.requireActual('../../src/utils/collection'); return { ...original, - getTreePathFromCollectionToItem: jest.fn(), - mergeVars: jest.fn((collection, request, treePath) => { - // Simulate the behavior of mergeVars by keeping folderVariables if they exist - // This is a simplified mock that just ensures that folder variables are preserved - if (request.folderVariables) { - // We don't need to modify the request, just ensure folderVariables remain - } + getTreePathFromCollectionToItem: jest.fn().mockReturnValue([]), + mergeVars: jest.fn(), + getEnvVars: jest.fn(env => { + if (!env || !env.variables) return {}; + return env.variables.reduce((acc, variable) => { + if (variable.enabled) { + acc[variable.name] = variable.value; + } + return acc; + }, {}); }) }; }); -describe('Prepare GQL Introspection Request', () => { +jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { + return jest.fn().mockImplementation((endpoint, vars, request, root) => { + return { + url: endpoint, + method: 'POST', + headers: request?.headers || {}, + data: { + query: '{ __schema { types { name } } }' + } + }; + }); +}); + +const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); +const { fetchGqlSchemaHandler } = require('../../src/ipc/network'); +const { getTreePathFromCollectionToItem } = require('../../src/utils/collection'); + +describe('fetchGqlSchemaHandler', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('should receive combined variables from fetchGqlSchema', async () => { + it('should receive combined variables from fetchGqlSchemaHandler', async () => { const endpoint = 'https://example.com/'; const environment = { variables: [ @@ -289,26 +295,6 @@ describe('Prepare GQL Introspection Request', () => { } }; - // Make sure our mock properly returns the folder variables - prepareGqlIntrospectionRequest.mockImplementationOnce((endpoint, resolvedVars, req, root) => { - // In a real scenario, the resolvedVars would include the folder variables - // Simulate the correct merge of variables - const combinedVars = { - ...resolvedVars, - SHARED_VAR: 'folder-value' // This simulates the correct precedence - }; - - return { - method: 'POST', - url: endpoint, - headers: {}, - data: JSON.stringify(combinedVars) - }; - }); - - // Set up empty tree path - getTreePathFromCollectionToItem.mockReturnValue([]); - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( @@ -377,173 +363,6 @@ describe('Prepare GQL Introspection Request', () => { collection.root ); }); - - it('should properly respect the complete variable precedence hierarchy', async () => { - const endpoint = 'https://example.com/'; - const environment = { - variables: [ - { name: 'ENV_VAR', value: 'env-value', enabled: true }, - { name: 'SHARED_VAR', value: 'env-value', enabled: true } - ] - }; - const request = { - vars: { - REQUEST_VAR: 'request-value', - SHARED_VAR: 'request-value' - }, - folderVariables: { - FOLDER_VAR: 'folder-value', - SHARED_VAR: 'folder-value' - } - }; - const collection = { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: { - RUNTIME_VAR: 'runtime-value', - SHARED_VAR: 'runtime-value' - }, - globalEnvironmentVariables: { - GLOBAL_VAR: 'global-value', - SHARED_VAR: 'global-value' - }, - root: { - request: { - headers: [] - } - } - }; - - // Make sure our mock returns the variables with correct precedence - prepareGqlIntrospectionRequest.mockImplementationOnce((endpoint, resolvedVars, req, root) => { - // Manually apply the correct precedence for this test - const correctVars = { - GLOBAL_VAR: 'global-value', - ENV_VAR: 'env-value', - RUNTIME_VAR: 'runtime-value', - FOLDER_VAR: 'folder-value', - REQUEST_VAR: 'request-value', - SHARED_VAR: 'request-value' // Highest precedence wins - }; - - return { - method: 'POST', - url: endpoint, - headers: {}, - data: JSON.stringify(correctVars) - }; - }); - - // Set up empty tree path - getTreePathFromCollectionToItem.mockReturnValue([]); - - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - GLOBAL_VAR: 'global-value', - ENV_VAR: 'env-value', - RUNTIME_VAR: 'runtime-value', - FOLDER_VAR: 'folder-value', - REQUEST_VAR: 'request-value', - SHARED_VAR: 'request-value' // Shows highest precedence wins - }), - request, - collection.root - ); - }); }); -describe('GraphQL Schema Handler Header Tests', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - const createBasicSetup = () => ({ - endpoint: 'https://example.com/', - environment: { variables: [] }, - request: { vars: {}, headers: [] }, - collection: { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: {}, - globalEnvironmentVariables: {}, - root: { - request: { - headers: [] - } - } - } - }); - - it('should pass root headers to request', async () => { - const setup = createBasicSetup(); - setup.collection.root.request.headers = [ - { name: 'X-Root-Header', value: 'root-value', enabled: true } - ]; - - await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - setup.endpoint, - expect.any(Object), - setup.request, - setup.collection.root - ); - }); - - it('should pass request headers to request', async () => { - const setup = createBasicSetup(); - setup.request.headers = [ - { name: 'X-Request-Header', value: 'request-value', enabled: true } - ]; - - await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - setup.endpoint, - expect.any(Object), - setup.request, - setup.collection.root - ); - }); - - it('should handle environment variables in headers', async () => { - const setup = createBasicSetup(); - setup.environment.variables = [ - { name: 'AUTH_TOKEN', value: 'token-value', enabled: true } - ]; - setup.request.headers = [ - { name: 'Authorization', value: 'Bearer {{AUTH_TOKEN}}', enabled: true } - ]; - - await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - setup.endpoint, - expect.objectContaining({ - AUTH_TOKEN: 'token-value' - }), - setup.request, - setup.collection.root - ); - }); - - it('should handle enabled and disabled headers', async () => { - const setup = createBasicSetup(); - setup.request.headers = [ - { name: 'X-Enabled', value: 'enabled', enabled: true }, - { name: 'X-Disabled', value: 'disabled', enabled: false } - ]; - - await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - setup.endpoint, - expect.any(Object), - setup.request, - setup.collection.root - ); - }); -}); From 91397eaf5732062cab36d48ab0857015558bb58c Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 16:38:09 +0530 Subject: [PATCH 25/31] Renamed fetchGqlSchema to fetchGqlSchemaHandler --- packages/bruno-electron/src/ipc/network/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index dc76d9690..e25d02899 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -1327,4 +1327,4 @@ const registerNetworkIpc = (mainWindow) => { module.exports = registerNetworkIpc; module.exports.configureRequest = configureRequest; module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig; -module.exports.fetchGqlSchema = fetchGqlSchema; +module.exports.fetchGqlSchemaHandler = fetchGqlSchemaHandler; From 788569a5f469e5f6930065577891863767f86500 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 16:39:07 +0530 Subject: [PATCH 26/31] Added testcases for prepare-gql-introspection-request.spec.js --- .../prepare-gql-introspection-request.spec.js | 120 +++++++++--------- 1 file changed, 59 insertions(+), 61 deletions(-) diff --git a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js index a541b9f2f..2eacde679 100644 --- a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js +++ b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js @@ -1,68 +1,66 @@ -const { interpolate } = require('@usebruno/common'); -const { setAuthHeaders } = require('../../src/ipc/network/prepare-request'); const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); -const { fetchGqlSchema } = require('../../src/ipc/network'); -// Mock the module -jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { - return jest.fn().mockReturnValue({ - method: 'POST', - url: 'https://example.com/', - headers: {}, - data: '{}' - }); -}); - -describe('Prepare GQL Introspection Request', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should receive combined variables from fetchGqlSchema', async () => { - const endpoint = 'https://example.com/'; - const environment = { - variables: [ - { name: 'API_TOKEN', value: 'secret-token', enabled: true }, - { name: 'ENV_VAR', value: 'env-value', enabled: true } - ] - }; - const request = { - vars: { - requestVar: 'request-value' - }, - headers: [ - { name: 'Authorization', value: 'Bearer {{API_TOKEN}}', enabled: true } - ] - }; - const collection = { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: { - runtimeVar: 'runtime-value' - }, - globalEnvironmentVariables: { - globalVar: 'global-value' - }, - root: { - request: { - headers: [] - } +describe('prepareGqlIntrospectionRequest', () => { + const createBasicSetup = () => ({ + endpoint: 'https://example.com/', + request: { + headers: [] + }, + collectionRoot: { + request: { + headers: [] } + } + }); + + it('should handle environment variables in headers', () => { + const setup = createBasicSetup(); + setup.request.headers = [ + { name: 'Authorization', value: 'Bearer {{AUTH_TOKEN}}', enabled: true } + ]; + const vars = { + AUTH_TOKEN: 'token-value' }; - await fetchGqlSchema(endpoint, environment, request, collection); + const result = prepareGqlIntrospectionRequest(setup.endpoint, vars, setup.request, setup.collectionRoot); - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - API_TOKEN: 'secret-token', - ENV_VAR: 'env-value', - requestVar: 'request-value', - runtimeVar: 'runtime-value', - globalVar: 'global-value' - }), - request, - collection.root - ); + expect(result.headers['Authorization']).toBe('Bearer token-value'); + expect(result.method).toBe('POST'); + expect(result.url).toBe(setup.endpoint); }); -}); + + it('should override collection headers with request headers', () => { + const setup = createBasicSetup(); + setup.collectionRoot.request.headers = [ + { name: 'X-Header', value: 'collection-value', enabled: true } + ]; + setup.request.headers = [ + { name: 'X-Header', value: 'request-value', enabled: true } + ]; + + const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot); + + expect(result.headers['X-Header']).toBe('request-value'); + }); + + it('should handle enabled and disabled headers', () => { + const setup = createBasicSetup(); + setup.request.headers = [ + { name: 'X-Enabled', value: 'enabled', enabled: true }, + { name: 'X-Disabled', value: 'disabled', enabled: false } + ]; + + const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot); + + expect(result.headers['X-Enabled']).toBe('enabled'); + expect(result.headers['X-Disabled']).toBeUndefined(); + }); + + it('should always include required GraphQL headers', () => { + const setup = createBasicSetup(); + const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot); + expect(result.headers['Accept']).toBe('application/json'); + expect(result.headers['Content-Type']).toBe('application/json'); + }); + +}); \ No newline at end of file From ce1110bdd45c0b35da4d4ec6269e6d29810d097c Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 16:39:40 +0530 Subject: [PATCH 27/31] Added interpolate for header values --- .../src/ipc/network/prepare-gql-introspection-request.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js index 851069bb3..158a71dc6 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js @@ -16,7 +16,7 @@ const prepareGqlIntrospectionRequest = (endpoint, resolvedVars, request, collect method: 'POST', url: endpoint, headers: { - ...mapHeaders(request.headers, get(collectionRoot, 'request.headers', [])), + ...mapHeaders(request.headers, get(collectionRoot, 'request.headers', []), resolvedVars), Accept: 'application/json', 'Content-Type': 'application/json' }, @@ -26,20 +26,20 @@ const prepareGqlIntrospectionRequest = (endpoint, resolvedVars, request, collect return setAuthHeaders(axiosRequest, request, collectionRoot); }; -const mapHeaders = (requestHeaders, collectionHeaders) => { +const mapHeaders = (requestHeaders, collectionHeaders, resolvedVars) => { const headers = {}; // Add collection headers first each(collectionHeaders, (h) => { if (h.enabled) { - headers[h.name] = h.value; + headers[h.name] = interpolate(h.value, resolvedVars); } }); // Then add request headers, which will overwrite if names overlap each(requestHeaders, (h) => { if (h.enabled) { - headers[h.name] = h.value; + headers[h.name] = interpolate(h.value, resolvedVars); } }); From 9f1aed32097bcfdaf379cd7d05c58ee0810bd584 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 16:42:18 +0530 Subject: [PATCH 28/31] Refactored fetch-gql-schema-handler.spec.js --- .../network/fetch-gql-schema-handler.spec.js | 34 ++++--------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js index 3104807e8..250237e32 100644 --- a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js +++ b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js @@ -1,3 +1,6 @@ +const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); +const { fetchGqlSchemaHandler } = require('../../src/ipc/network'); + // Mock the modules first, before requiring them jest.mock('../../src/utils/collection', () => { const original = jest.requireActual('../../src/utils/collection'); @@ -30,15 +33,15 @@ jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { }); }); -const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); -const { fetchGqlSchemaHandler } = require('../../src/ipc/network'); -const { getTreePathFromCollectionToItem } = require('../../src/utils/collection'); - describe('fetchGqlSchemaHandler', () => { beforeEach(() => { jest.clearAllMocks(); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('should receive combined variables from fetchGqlSchemaHandler', async () => { const endpoint = 'https://example.com/'; const environment = { @@ -71,9 +74,6 @@ describe('fetchGqlSchemaHandler', () => { } }; - // Set up empty tree path since we don't need it for this test - getTreePathFromCollectionToItem.mockReturnValue([]); - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( @@ -332,26 +332,6 @@ describe('fetchGqlSchemaHandler', () => { } }; - // Make sure our mock properly returns the folder variables with correct precedence - prepareGqlIntrospectionRequest.mockImplementationOnce((endpoint, resolvedVars, req, root) => { - // In a real scenario, the resolvedVars would include the folder variables - // Simulate the correct merge of variables - const combinedVars = { - ...resolvedVars, - SHARED_VAR: 'folder-value' // This simulates the correct precedence - }; - - return { - method: 'POST', - url: endpoint, - headers: {}, - data: JSON.stringify(combinedVars) - }; - }); - - // Set up empty tree path - getTreePathFromCollectionToItem.mockReturnValue([]); - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( From afb2d3dffd1e7398261409f84e22e2d0563b80a4 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 22:52:37 +0530 Subject: [PATCH 29/31] Updated resolved variable assignment and testcases --- .../bruno-electron/src/ipc/network/index.js | 9 +- .../network/fetch-gql-schema-handler.spec.js | 420 ++++++++---------- 2 files changed, 199 insertions(+), 230 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index e25d02899..3adb5ea88 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -329,16 +329,17 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col const globalEnvironmentVars = collection.globalEnvironmentVariables; const collectionRuntimeVars = collection.runtimeVariables; const folderVars = resolvedRequest.folderVariables; - const requestRuntimeVars = resolvedRequest.vars; + const requestVariables = resolvedRequest.requestVariables; + const collectionVariables = resolvedRequest.collectionVariables; - // Precedence: globalEnvironmentVars < envVars < collectionEnvVars < collectionRunTimeVars < folderVars < requestRunTimeVars + // Precedence: globalEnvironmentVars < collectionVariables < envVars < folderVars < requestVariables const resolvedVars = merge( {}, globalEnvironmentVars, + collectionVariables, envVars, - collectionRuntimeVars, folderVars, - requestRuntimeVars + requestVariables ); const collectionRoot = get(collection, 'root', {}); diff --git a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js index 250237e32..fdfff7b89 100644 --- a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js +++ b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js @@ -1,25 +1,7 @@ const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); const { fetchGqlSchemaHandler } = require('../../src/ipc/network'); -// Mock the modules first, before requiring them -jest.mock('../../src/utils/collection', () => { - const original = jest.requireActual('../../src/utils/collection'); - return { - ...original, - getTreePathFromCollectionToItem: jest.fn().mockReturnValue([]), - mergeVars: jest.fn(), - getEnvVars: jest.fn(env => { - if (!env || !env.variables) return {}; - return env.variables.reduce((acc, variable) => { - if (variable.enabled) { - acc[variable.name] = variable.value; - } - return acc; - }, {}); - }) - }; -}); - +// Mock only the prepare-gql-introspection-request to avoid network calls jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { return jest.fn().mockImplementation((endpoint, vars, request, root) => { return { @@ -42,54 +24,6 @@ describe('fetchGqlSchemaHandler', () => { jest.restoreAllMocks(); }); - it('should receive combined variables from fetchGqlSchemaHandler', async () => { - const endpoint = 'https://example.com/'; - const environment = { - variables: [ - { name: 'API_TOKEN', value: 'secret-token', enabled: true }, - { name: 'ENV_VAR', value: 'env-value', enabled: true } - ] - }; - const request = { - vars: { - requestVar: 'request-value' - }, - headers: [ - { name: 'Authorization', value: 'Bearer {{API_TOKEN}}', enabled: true } - ] - }; - const collection = { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: { - runtimeVar: 'runtime-value' - }, - globalEnvironmentVariables: { - globalVar: 'global-value' - }, - root: { - request: { - headers: [] - } - } - }; - - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - API_TOKEN: 'secret-token', - ENV_VAR: 'env-value', - requestVar: 'request-value', - runtimeVar: 'runtime-value', - globalVar: 'global-value' - }), - request, - collection.root - ); - }); - it('should override global environment variables with environment variables', async () => { const endpoint = 'https://example.com/'; const environment = { @@ -98,7 +32,10 @@ describe('fetchGqlSchemaHandler', () => { ] }; const request = { - vars: {} + uid: 'test-request', + vars: { + req: [] // No request variables + } }; const collection = { uid: 'test-collection', @@ -107,9 +44,22 @@ describe('fetchGqlSchemaHandler', () => { globalEnvironmentVariables: { SHARED_VAR: 'global-value' }, + items: [ + { + uid: 'test-request', + request: { + vars: { + req: [] // No request variables + } + } + } + ], root: { request: { - headers: [] + headers: [], + vars: { + req: [] // No collection variables + } } } }; @@ -126,150 +76,6 @@ describe('fetchGqlSchemaHandler', () => { ); }); - it('should override environment variables with collection runtime variables', async () => { - const endpoint = 'https://example.com/'; - const environment = { - variables: [ - { name: 'SHARED_VAR', value: 'env-value', enabled: true } - ] - }; - const request = { - vars: {} - }; - const collection = { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: { - SHARED_VAR: 'runtime-value' - }, - globalEnvironmentVariables: {}, - root: { - request: { - headers: [] - } - } - }; - - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - SHARED_VAR: 'runtime-value' - }), - request, - collection.root - ); - }); - - it('should override collection runtime variables with request runtime variables', async () => { - const endpoint = 'https://example.com/'; - const environment = { - variables: [] - }; - const request = { - vars: { - SHARED_VAR: 'request-value' - } - }; - const collection = { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: { - SHARED_VAR: 'runtime-value' - }, - globalEnvironmentVariables: {}, - root: { - request: { - headers: [] - } - } - }; - - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - SHARED_VAR: 'request-value' - }), - request, - collection.root - ); - }); - - it('should override global environment with request runtime variables', async () => { - const endpoint = 'https://example.com/'; - const environment = { - variables: [] - }; - const request = { - vars: { - SHARED_VAR: 'request-value' - } - }; - const collection = { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: {}, - globalEnvironmentVariables: { - SHARED_VAR: 'global-value' - }, - root: { - request: { - headers: [] - } - } - }; - - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - SHARED_VAR: 'request-value' - }), - request, - collection.root - ); - }); - - it('should override global environment with collection runtime variables', async () => { - const endpoint = 'https://example.com/'; - const environment = { - variables: [] - }; - const request = { - vars: {} - }; - const collection = { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: { - SHARED_VAR: 'runtime-value' - }, - globalEnvironmentVariables: { - SHARED_VAR: 'global-value' - }, - root: { - request: { - headers: [] - } - } - }; - - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - SHARED_VAR: 'runtime-value' - }), - request, - collection.root - ); - }); - it('should override environment variables with folder-level variables', async () => { const endpoint = 'https://example.com/'; const environment = { @@ -278,9 +84,9 @@ describe('fetchGqlSchemaHandler', () => { ] }; const request = { - vars: {}, - folderVariables: { - SHARED_VAR: 'folder-value' + uid: 'test-request', + vars: { + req: [] // No request variables } }; const collection = { @@ -288,9 +94,37 @@ describe('fetchGqlSchemaHandler', () => { pathname: '/test', runtimeVariables: {}, globalEnvironmentVariables: {}, + items: [ + { + uid: 'test-folder', + type: 'folder', + root: { + request: { + vars: { + req: [ + { name: 'SHARED_VAR', value: 'folder-value', enabled: true } + ] + } + } + }, + items: [ + { + uid: 'test-request', + request: { + vars: { + req: [] // No request variables + } + } + } + ] + } + ], root: { request: { - headers: [] + headers: [], + vars: { + req: [] // No collection variables + } } } }; @@ -307,27 +141,57 @@ describe('fetchGqlSchemaHandler', () => { ); }); - it('should override collection runtime variables with folder-level variables', async () => { + it('should override folder-level variables with request variables', async () => { const endpoint = 'https://example.com/'; const environment = { variables: [] }; const request = { - vars: {}, - folderVariables: { - SHARED_VAR: 'folder-value' + uid: 'test-request', + vars: { + req: [ + { name: 'SHARED_VAR', value: 'request-value', enabled: true } + ] } }; const collection = { uid: 'test-collection', pathname: '/test', - runtimeVariables: { - SHARED_VAR: 'runtime-value' - }, + runtimeVariables: {}, globalEnvironmentVariables: {}, + items: [ + { + uid: 'test-folder', + type: 'folder', + root: { + request: { + vars: { + req: [ + { name: 'SHARED_VAR', value: 'folder-value', enabled: true } + ] + } + } + }, + items: [ + { + uid: 'test-request', + request: { + vars: { + req: [ + { name: 'SHARED_VAR', value: 'request-value', enabled: true } + ] + } + } + } + ] + } + ], root: { request: { - headers: [] + headers: [], + vars: { + req: [] // No collection variables + } } } }; @@ -337,7 +201,111 @@ describe('fetchGqlSchemaHandler', () => { expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( endpoint, expect.objectContaining({ - SHARED_VAR: 'folder-value' + SHARED_VAR: 'request-value' + }), + request, + collection.root + ); + }); + + it('should override global environment variables with collection variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [] + }; + const request = { + uid: 'test-request', + vars: { + req: [] // No request variables + } + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: {}, + globalEnvironmentVariables: { + SHARED_VAR: 'global-value' + }, + items: [ + { + uid: 'test-request', + request: { + vars: { + req: [] // No request variables + } + } + } + ], + root: { + request: { + headers: [], + vars: { + req: [ + { name: 'SHARED_VAR', value: 'collection-value', enabled: true } + ] + } + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'collection-value' + }), + request, + collection.root + ); + }); + + it('should override collection variables with environment variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [ + { name: 'SHARED_VAR', value: 'env-value', enabled: true } + ] + }; + const request = { + uid: 'test-request', + vars: { + req: [] // No request variables + } + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: {}, + globalEnvironmentVariables: {}, + items: [ + { + uid: 'test-request', + request: { + vars: { + req: [] // No request variables + } + } + } + ], + root: { + request: { + headers: [], + vars: { + req: [ + { name: 'SHARED_VAR', value: 'collection-value', enabled: true } + ] + } + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'env-value' }), request, collection.root From 8d5d9520260a2dad4f4535ec7d39954a1d9aacff Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Tue, 27 May 2025 14:38:48 +0530 Subject: [PATCH 30/31] Added runtimeVars in prepareGqlIntrospectionRequest --- .../bruno-electron/src/ipc/network/index.js | 6 +- .../network/fetch-gql-schema-handler.spec.js | 57 ++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 3adb5ea88..506fbfa3c 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -331,15 +331,17 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col const folderVars = resolvedRequest.folderVariables; const requestVariables = resolvedRequest.requestVariables; const collectionVariables = resolvedRequest.collectionVariables; + const runtimeVars = collection.runtimeVariables; - // Precedence: globalEnvironmentVars < collectionVariables < envVars < folderVars < requestVariables + // Precedence: runtimeVars > requestVariables > folderVars > envVars > collectionVariables > globalEnvironmentVars const resolvedVars = merge( {}, globalEnvironmentVars, collectionVariables, envVars, folderVars, - requestVariables + requestVariables, + runtimeVars ); const collectionRoot = get(collection, 'root', {}); diff --git a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js index fdfff7b89..8831ba48b 100644 --- a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js +++ b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js @@ -15,7 +15,7 @@ jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { }); }); -describe('fetchGqlSchemaHandler', () => { +describe('fetchGqlSchemaHandler - variable precedence', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -311,6 +311,61 @@ describe('fetchGqlSchemaHandler', () => { collection.root ); }); + + it('should override request variables with runtime variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [] + }; + + const request = { + uid: 'test-request', + vars: { + req: [ + { name: 'SHARED_VAR', value: 'request-value', enabled: true } + ] + } + }; + + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + SHARED_VAR: 'runtime-value' + }, + items: [ + { + uid: 'test-request', + request: { + vars: { + req: [ + { name: 'SHARED_VAR', value: 'request-value', enabled: true } + ] + } + } + } + ], + root: { + request: { + headers: [], + vars: { + req: [] // No collection variables + } + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'runtime-value' + }), + request, + collection.root + ); + }) }); From 6f9daadcfbe36e1560a9f95c8a5a719c2611c67f Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Tue, 27 May 2025 15:44:07 +0530 Subject: [PATCH 31/31] Update index.js Removed duplicate variable --- packages/bruno-electron/src/ipc/network/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 506fbfa3c..ca196f89b 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -327,7 +327,6 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col const envVars = getEnvVars(environment); const globalEnvironmentVars = collection.globalEnvironmentVariables; - const collectionRuntimeVars = collection.runtimeVariables; const folderVars = resolvedRequest.folderVariables; const requestVariables = resolvedRequest.requestVariables; const collectionVariables = resolvedRequest.collectionVariables;