diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js index 3b1cc6109..8fe747389 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js @@ -7,10 +7,8 @@ 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, pathname, uid } : { ...item.request, pathname, uid }; + const request = item.draft ? item.draft.request : item.request; let { schema, diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index c97b7f63c..b56cd4ac0 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, merge } = require('lodash'); +const { each, get, extend, cloneDeep } = require('lodash'); const { NtlmClient } = require('axios-ntlm'); const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js'); const { interpolateString } = require('./interpolate-string'); @@ -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, getTreePathFromCollectionToItem, mergeVars } = require('../../utils/collection'); +const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars } = require('../../utils/collection'); const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials } = require('../../utils/oauth2'); const { preferencesUtil } = require('../../store/preferences'); const { getProcessEnvVars } = require('../../store/process-env'); @@ -317,77 +317,6 @@ const configureRequest = async ( return axiosInstance; }; -const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, collection) => { - try { - 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 globalEnvironmentVars = collection.globalEnvironmentVariables; - const folderVars = resolvedRequest.folderVariables; - const requestVariables = resolvedRequest.requestVariables; - const collectionVariables = resolvedRequest.collectionVariables; - const runtimeVars = collection.runtimeVariables; - - // Precedence: runtimeVars > requestVariables > folderVars > envVars > collectionVariables > globalEnvironmentVars - const resolvedVars = merge( - {}, - globalEnvironmentVars, - collectionVariables, - envVars, - folderVars, - requestVariables, - runtimeVars - ); - - const collectionRoot = get(collection, 'root', {}); - const request = prepareGqlIntrospectionRequest(endpoint, resolvedVars, _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); @@ -875,8 +804,84 @@ const registerNetworkIpc = (mainWindow) => { }); }); - // handler for fetch-gql-schema - ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler) + ipcMain.handle('fetch-gql-schema', async (event, endpoint, environment, _request, collection) => { + try { + const envVars = getEnvVars(environment); + const collectionRoot = get(collection, 'root', {}); + const request = prepareGqlIntrospectionRequest(endpoint, envVars, _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); + } + }); ipcMain.handle( 'renderer:run-collection-folder', @@ -1337,4 +1342,3 @@ const registerNetworkIpc = (mainWindow) => { module.exports = registerNetworkIpc; module.exports.configureRequest = configureRequest; module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig; -module.exports.fetchGqlSchemaHandler = fetchGqlSchemaHandler; 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 158a71dc6..c137c4b33 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, resolvedVars, request, collectionRoot) => { +const prepareGqlIntrospectionRequest = (endpoint, envVars, request, collectionRoot) => { if (endpoint && endpoint.length) { - endpoint = interpolate(endpoint, resolvedVars); + endpoint = interpolate(endpoint, envVars); } const queryParams = { @@ -16,7 +16,7 @@ const prepareGqlIntrospectionRequest = (endpoint, resolvedVars, request, collect method: 'POST', url: endpoint, headers: { - ...mapHeaders(request.headers, get(collectionRoot, 'request.headers', []), resolvedVars), + ...mapHeaders(request.headers, get(collectionRoot, 'request.headers', [])), Accept: 'application/json', 'Content-Type': 'application/json' }, @@ -26,20 +26,19 @@ const prepareGqlIntrospectionRequest = (endpoint, resolvedVars, request, collect return setAuthHeaders(axiosRequest, request, collectionRoot); }; -const mapHeaders = (requestHeaders, collectionHeaders, resolvedVars) => { +const mapHeaders = (requestHeaders, collectionHeaders) => { const headers = {}; - // Add collection headers first - each(collectionHeaders, (h) => { + each(requestHeaders, (h) => { if (h.enabled) { - headers[h.name] = interpolate(h.value, resolvedVars); + headers[h.name] = h.value; } }); - // Then add request headers, which will overwrite if names overlap - each(requestHeaders, (h) => { + // collection headers + each(collectionHeaders, (h) => { if (h.enabled) { - headers[h.name] = interpolate(h.value, resolvedVars); + headers[h.name] = h.value; } }); 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 deleted file mode 100644 index 8831ba48b..000000000 --- a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js +++ /dev/null @@ -1,371 +0,0 @@ -const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); -const { fetchGqlSchemaHandler } = require('../../src/ipc/network'); - -// 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 { - url: endpoint, - method: 'POST', - headers: request?.headers || {}, - data: { - query: '{ __schema { types { name } } }' - } - }; - }); -}); - -describe('fetchGqlSchemaHandler - variable precedence', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - 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 = { - 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: [] // No collection variables - } - } - } - }; - - 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 folder-level 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-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: [], - vars: { - req: [] // No collection variables - } - } - } - }; - - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - SHARED_VAR: 'folder-value' - }), - request, - collection.root - ); - }); - - it('should override folder-level variables with request 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: {}, - 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: [], - vars: { - req: [] // No collection variables - } - } - } - }; - - 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 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 - ); - }); - - 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 - ); - }) -}); - - 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 deleted file mode 100644 index 2eacde679..000000000 --- a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js +++ /dev/null @@ -1,66 +0,0 @@ -const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); - -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' - }; - - const result = prepareGqlIntrospectionRequest(setup.endpoint, vars, setup.request, setup.collectionRoot); - - 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