From 0b19b26ce78978cd3b32f5512237b3c11db20416 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Thu, 6 Feb 2025 19:41:38 +0530 Subject: [PATCH] feat: yaml lang - parseRequest and stringifyRequest --- packages/bruno-lang/v3/src/common.js | 346 ++++++++++++++++++ packages/bruno-lang/v3/src/index.js | 8 + packages/bruno-lang/v3/src/parseRequest.js | 235 ++++++++++++ .../bruno-lang/v3/src/stringifyRequest.js | 54 +++ .../__fixtures__/graphql-request.json | 38 ++ .../__fixtures__/graphql-request.yml | 36 ++ .../__fixtures__/http-request.json | 70 ++++ .../__fixtures__/http-request.yml | 58 +++ .../v3/tests/stringifyRequest/graphql.test.js | 103 ++++++ .../v3/tests/stringifyRequest/http.test.js | 244 ++++++++++++ .../bruno-schema/src/collections/index.js | 5 +- 11 files changed, 1195 insertions(+), 2 deletions(-) create mode 100644 packages/bruno-lang/v3/src/common.js create mode 100644 packages/bruno-lang/v3/src/index.js create mode 100644 packages/bruno-lang/v3/src/parseRequest.js create mode 100644 packages/bruno-lang/v3/src/stringifyRequest.js create mode 100644 packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/graphql-request.json create mode 100644 packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/graphql-request.yml create mode 100644 packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/http-request.json create mode 100644 packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/http-request.yml create mode 100644 packages/bruno-lang/v3/tests/stringifyRequest/graphql.test.js create mode 100644 packages/bruno-lang/v3/tests/stringifyRequest/http.test.js diff --git a/packages/bruno-lang/v3/src/common.js b/packages/bruno-lang/v3/src/common.js new file mode 100644 index 000000000..b65993ea5 --- /dev/null +++ b/packages/bruno-lang/v3/src/common.js @@ -0,0 +1,346 @@ +const _ = require('lodash'); + +const getMeta = (json) => { + const sequence = _.get(json, 'seq'); + const meta = { + name: _.get(json, 'name') + }; + + const description = _.get(json, 'description'); + if (description) { + meta.description = description; + } + + meta.seq = !isNaN(sequence) ? Number(sequence) : 1; + + return meta; +}; + +const getParams = (req) => { + return { + query: _.map(_.filter(req?.params || [], param => param.type === 'query'), (param) => { + const paramObj = { + name: param.name, + value: param.value, + type: param.type + }; + + if (param.description) { + paramObj.description = param.description; + } + + if (param.enabled === false) { + paramObj.disabled = true; + } + + return paramObj; + }), + path: _.map(_.filter(req?.params || [], param => param.type === 'path'), (param) => { + const paramObj = { + name: param.name, + value: param.value, + type: param.type + }; + + if (param.description) { + paramObj.description = param.description; + } + + if (param.enabled === false) { + paramObj.disabled = true; + } + + return paramObj; + }) + } +}; + +const getHeaders = (req) => { + return _.map(_.get(req, 'headers', []), (header) => { + const headerObj = { + name: header.name, + value: header.value, + }; + + if (header.description) { + headerObj.description = header.description; + } + + if (header.enabled === false) { + headerObj.disabled = true; + } + + return headerObj; + }); +}; + +const getBody = (req) => { + const body = _.get(req, 'body', {}); + const mode = _.get(body, 'mode', 'none'); + + if (mode === 'none') { + return null; + } + + if (mode === 'graphql') { + return { + type: 'graphql', + query: _.get(body, 'graphql.query', ''), + variables: _.get(body, 'graphql.variables', '') + }; + } + + if (mode === 'sparql') { + return { + type: 'sparql', + query: _.get(body, 'sparql', '') + }; + } + + if (mode === 'formUrlEncoded') { + return { + type: 'form-urlencoded', + data: _.map(_.get(body, 'formUrlEncoded', []), (param) => { + const paramObj = { + name: param.name, + value: param.value + }; + + if (param.description) { + paramObj.description = param.description; + } + + if (param.enabled === false) { + paramObj.disabled = true; + } + + return paramObj; + }) + }; + } + + if (mode === 'multipartForm') { + return { + type: 'multipart-form', + data: _.map(_.get(body, 'multipartForm', []), (param) => { + const paramObj = { + name: param.name, + value: param.value, + type: param.type + }; + + if (param.description) { + paramObj.description = param.description; + } + + if (param.enabled === false) { + paramObj.disabled = true; + } + + if (param.contentType) { + paramObj.content_type = param.contentType; + } + + return paramObj; + }) + }; + } + + let data = ''; + switch(mode) { + case 'json': + data = _.get(body, 'json', ''); + break; + case 'text': + data = _.get(body, 'text', ''); + break; + case 'xml': + data = _.get(body, 'xml', ''); + break; + } + + return { + type: mode, + data + }; +}; + +const getAuth = (req) => { + const auth = {}; + const mode = _.get(req, 'auth.mode', 'none'); + + if (req?.auth?.awsv4) { + auth.awsv4 = { + access_key_id: req?.auth?.awsv4?.accessKeyId, + secret_access_key: req?.auth?.awsv4?.secretAccessKey, + session_token: req?.auth?.awsv4?.sessionToken, + service: req?.auth?.awsv4?.service, + region: req?.auth?.awsv4?.region, + profile_name: req?.auth?.awsv4?.profileName + }; + } + + if (req?.auth?.basic) { + auth.basic = { + username: req?.auth?.basic?.username, + password: req?.auth?.basic?.password + }; + } + + if (req?.auth?.bearer) { + auth.bearer = { + token: req?.auth?.bearer?.token + }; + } + + if (req?.auth?.digest) { + auth.digest = { + username: req?.auth?.digest?.username, + password: req?.auth?.digest?.password + }; + } + + if (req?.auth?.ntlm) { + auth.ntlm = { + username: req?.auth?.ntlm?.username, + password: req?.auth?.ntlm?.password, + domain: req?.auth?.ntlm?.domain + }; + } + + if (req?.auth?.oauth2) { + auth.oauth2 = {}; + + if (req?.auth?.oauth2?.grantType === 'password') { + auth.oauth2 = { + grant_type: 'password', + access_token_url: req?.auth?.oauth2?.accessTokenUrl || '', + username: req?.auth?.oauth2?.username || '', + password: req?.auth?.oauth2?.password || '', + client_id: req?.auth?.oauth2?.clientId || '', + client_secret: req?.auth?.oauth2?.clientSecret || '', + scope: req?.auth?.oauth2?.scope || '' + }; + } + + if (req?.auth?.oauth2?.grantType === 'authorization_code') { + auth.oauth2 = { + grant_type: 'authorization_code', + callback_url: req?.auth?.oauth2?.callbackUrl || '', + authorization_url: req?.auth?.oauth2?.authorizationUrl || '', + access_token_url: req?.auth?.oauth2?.accessTokenUrl || '', + client_id: req?.auth?.oauth2?.clientId || '', + client_secret: req?.auth?.oauth2?.clientSecret || '', + scope: req?.auth?.oauth2?.scope || '', + state: req?.auth?.oauth2?.state || '', + pkce: req?.auth?.oauth2?.pkce || false + }; + } + + if (req?.auth?.oauth2?.grantType === 'client_credentials') { + auth.oauth2 = { + grant_type: 'client_credentials', + access_token_url: req?.auth?.oauth2?.accessTokenUrl || '', + client_id: req?.auth?.oauth2?.clientId || '', + client_secret: req?.auth?.oauth2?.clientSecret || '', + scope: req?.auth?.oauth2?.scope || '' + }; + } + } + + if (req?.auth?.apikey) { + auth.apikey = { + key: req?.auth?.apikey?.key, + value: req?.auth?.apikey?.value, + placement: req?.auth?.apikey?.placement + }; + } + + return { + type: mode, + ...auth + }; +}; + +const getVars = (req) => { + const preRequest = _.map(_.get(req, 'vars.req', []), (variable) => { + const varObj = { + name: variable.name, + value: variable.value + }; + + if (variable.description) { + varObj.description = variable.description; + } + + if (variable.enabled === false) { + varObj.disabled = true; + } + + return varObj; + }); + + const postResponse = _.map(_.get(req, 'vars.res', []), (variable) => { + const varObj = { + name: variable.name, + value: variable.value + }; + + if (variable.description) { + varObj.description = variable.description; + } + + if (variable.enabled === false) { + varObj.disabled = true; + } + + return varObj; + }); + + const vars = {}; + + if(preRequest.length) { + vars['pre-request'] = preRequest; + } + + if(postResponse.length) { + vars['post-response'] = postResponse; + } + + return !_.isEmpty(vars) ? vars : null; +}; + +const getScripts = (req) => { + const preRequestScript = _.get(req, 'script.req', ''); + const postResponseScript = _.get(req, 'script.res', ''); + + const scripts = {}; + if (preRequestScript) { + scripts['pre-request'] = preRequestScript; + } + if (postResponseScript) { + scripts['post-response'] = postResponseScript; + } + + return !_.isEmpty(scripts) ? scripts : null; +}; + +const getTests = (req) => { + return _.get(req, 'tests', ''); +}; + +const getDocs = (req) => { + return _.get(req, 'docs', ''); +}; + +module.exports = { + getMeta, + getParams, + getHeaders, + getBody, + getAuth, + getVars, + getScripts, + getTests, + getDocs +}; \ No newline at end of file diff --git a/packages/bruno-lang/v3/src/index.js b/packages/bruno-lang/v3/src/index.js new file mode 100644 index 000000000..3a3cadaa7 --- /dev/null +++ b/packages/bruno-lang/v3/src/index.js @@ -0,0 +1,8 @@ +const _ = require('lodash'); +const stringifyRequest = require('./stringifyRequest'); +const parseRequest = require('./parseRequest'); + +module.exports = { + parseRequest, + stringifyRequest, +}; diff --git a/packages/bruno-lang/v3/src/parseRequest.js b/packages/bruno-lang/v3/src/parseRequest.js new file mode 100644 index 000000000..8c6dc40ca --- /dev/null +++ b/packages/bruno-lang/v3/src/parseRequest.js @@ -0,0 +1,235 @@ +const yaml = require('js-yaml'); +const _ = require('lodash'); + +const parseRequest = (yamlContent) => { + const yamlData = yaml.load(yamlContent); + const isHttp = !!yamlData.http; + const request = isHttp ? yamlData.http : yamlData.graphql; + + const item = { + name: yamlData.meta.name, + description: yamlData.meta.description || '', + seq: yamlData.meta.seq, + type: isHttp ? 'http-request' : 'graphql-request' + }; + + item.request = { + method: request.method.toUpperCase(), + url: request.url, + headers: _.map(request.headers || [], header => ({ + name: header.name, + value: header.value, + description: header.description || '', + enabled: !header.disabled + })), + params: _.flatMap(request.params || {}, (params, type) => { + return _.map(params, param => ({ + name: param.name, + value: param.value, + type, + description: param.description || '', + enabled: !param.disabled + })); + }), + body: { + mode: 'none', + json: null, + text: null, + xml: null, + sparql: null, + formUrlEncoded: [], + multipartForm: [], + graphql: null + }, + auth: { + mode: 'none', + awsv4: null, + basic: null, + bearer: null, + ntlm: null, + digest: null, + oauth2: null, + wsse: null, + apikey: null + }, + vars: { + req: [], + res: [] + }, + script: { + req: '', + res: '' + }, + tests: '', + docs: '' + }; + + // Handle body + if (request.body) { + item.request.body.mode = request.body.type; + switch(request.body.type) { + case 'json': + item.request.body.json = request.body.data; + break; + case 'text': + item.request.body.text = request.body.data; + break; + case 'xml': + item.request.body.xml = request.body.data; + break; + case 'sparql': + item.request.body.sparql = request.body.data; + break; + case 'formUrlEncoded': + item.request.body.formUrlEncoded = _.map(request.body.data, formItem => ({ + name: formItem.name, + value: formItem.value, + description: formItem.description || '', + enabled: !formItem.disabled + })); + break; + case 'multipartForm': + item.request.body.multipartForm = _.map(request.body.data, formItem => ({ + name: formItem.name, + value: formItem.value, + description: formItem.description || '', + enabled: !formItem.disabled, + contentType: formItem.content_type || '' + })); + break; + case 'graphql': + item.request.body.graphql = { + query: request.body.data.query || '', + variables: request.body.data.variables || '' + }; + break; + } + } + + // Handle auth + if (request.auth) { + item.request.auth.mode = request.auth.type; + + switch(request.auth.type) { + case 'awsv4': + item.request.auth.awsv4 = { + accessKeyId: request.auth.access_key_id, + secretAccessKey: request.auth.secret_access_key, + sessionToken: request.auth.session_token, + service: request.auth.service, + region: request.auth.region, + profileName: request.auth.profile_name + } + break; + case 'basic': + item.request.auth.basic = { + username: request.auth.username, + password: request.auth.password + } + break; + case 'bearer': + item.request.auth.bearer = { + token: request.auth.token + } + break; + case 'digest': + item.request.auth.digest = { + username: request.auth.username, + password: request.auth.password + } + break; + case 'ntlm': + item.request.auth.ntlm = { + username: request.auth.username, + password: request.auth.password, + domain: request.auth.domain + } + break; + case 'wsse': + item.request.auth.wsse = { + username: request.auth.username, + password: request.auth.password + } + break; + case 'apikey': + item.request.auth.apikey = { + key: request.auth.key, + value: request.auth.value, + placement: request.auth.placement + } + break; + case 'oauth2': + if (request.auth.grant_type === 'password') { + item.request.auth.oauth2 = { + grantType: 'password', + accessTokenUrl: request.auth.access_token_url || '', + clientId: request.auth.client_id || '', + clientSecret: request.auth.client_secret || '', + scope: request.auth.scope || '', + username: request.auth.username || '', + password: request.auth.password || '' + }; + } else if (request.auth.grant_type === 'authorization_code') { + item.request.auth.oauth2 = { + grantType: 'authorization_code', + accessTokenUrl: request.auth.access_token_url || '', + clientId: request.auth.client_id || '', + clientSecret: request.auth.client_secret || '', + scope: request.auth.scope || '', + callbackUrl: request.auth.callback_url || '', + authorizationUrl: request.auth.authorization_url || '', + state: request.auth.state || '', + pkce: request.auth.pkce || false + }; + } else if (request.auth.grant_type === 'client_credentials') { + item.request.auth.oauth2 = { + grantType: 'client_credentials', + accessTokenUrl: request.auth.access_token_url || '', + clientId: request.auth.client_id || '', + clientSecret: request.auth.client_secret || '', + scope: request.auth.scope || '' + }; + } + break; + } + } + + // Handle vars + if (yamlData.vars) { + item.request.vars = { + req: _.map(yamlData.vars['pre-request'] || [], v => ({ + name: v.name, + value: v.value, + description: v.description || '', + enabled: !v.disabled + })), + res: _.map(yamlData.vars['post-response'] || [], v => ({ + name: v.name, + value: v.value, + description: v.description || '', + enabled: !v.disabled + })) + }; + } + + // Handle scripts + if (yamlData.scripts) { + item.request.script = { + req: yamlData.scripts['pre-request'] || '', + res: yamlData.scripts['post-response'] || '' + }; + } + + // Handle tests and docs + if (yamlData.tests) { + item.request.tests = yamlData.tests; + } + + if (yamlData.docs) { + item.request.docs = yamlData.docs; + } + + return item; +}; + +module.exports = parseRequest; \ No newline at end of file diff --git a/packages/bruno-lang/v3/src/stringifyRequest.js b/packages/bruno-lang/v3/src/stringifyRequest.js new file mode 100644 index 000000000..e1ad4e711 --- /dev/null +++ b/packages/bruno-lang/v3/src/stringifyRequest.js @@ -0,0 +1,54 @@ +const yaml = require('js-yaml'); +const _ = require('lodash'); +const { getMeta, getParams, getHeaders, getVars, getScripts, getBody, getAuth, getTests, getDocs } = require('./common'); + +const stringifyRequest = (json) => { + const request = json?.request; + const requestBody = getBody(request); + const requestAuth = getAuth(request); + const isGraphql = requestBody?.type === 'graphql'; + + const requestData = { + method: _.lowerCase(_.get(json, 'request.method')), + url: _.get(json, 'request.url'), + params: getParams(request), + headers: getHeaders(request), + }; + + if (requestBody && requestBody.type !== 'none') { + requestData.body = requestBody; + } + + if (requestAuth && requestAuth.mode !== 'none') { + requestData.auth = requestAuth; + } + + const finalJson = { + meta: getMeta(json), + [isGraphql ? 'graphql' : 'http']: requestData + }; + + const vars = getVars(request); + if (vars) { + finalJson.vars = vars; + } + + const scripts = getScripts(request); + if (scripts) { + finalJson.scripts = scripts; + } + + const tests = getTests(request); + if (tests) { + finalJson.tests = tests; + } + + const docs = getDocs(request); + if (docs) { + finalJson.docs = docs; + } + + return yaml.dump(finalJson); +}; + +module.exports = stringifyRequest; \ No newline at end of file diff --git a/packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/graphql-request.json b/packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/graphql-request.json new file mode 100644 index 000000000..5d7c5c777 --- /dev/null +++ b/packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/graphql-request.json @@ -0,0 +1,38 @@ +{ + "name": "Get User GraphQL", + "seq": 1, + "request": { + "method": "POST", + "url": "https://api.example.com/graphql", + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "graphql", + "graphql": { + "query": "query GetUser($id: ID!) {\n user(id: $id) {\n id\n name\n email\n }\n}", + "variables": "{\n \"id\": \"123\"\n}" + } + }, + "auth": { + "mode": "bearer", + "bearer": { + "token": "jwt-token" + } + }, + "vars": { + "req": [ + { + "name": "userId", + "value": "123" + } + ] + }, + "script": { + "req": "// Pre-request script for GraphQL" + } + } +} \ No newline at end of file diff --git a/packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/graphql-request.yml b/packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/graphql-request.yml new file mode 100644 index 000000000..bbb931663 --- /dev/null +++ b/packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/graphql-request.yml @@ -0,0 +1,36 @@ +meta: + name: Get User GraphQL + seq: 1 +graphql: + method: post + url: https://api.example.com/graphql + params: + query: [] + path: [] + headers: + - name: Content-Type + value: application/json + body: + type: graphql + query: |- + query GetUser($id: ID!) { + user(id: $id) { + id + name + email + } + } + variables: |- + { + "id": "123" + } + auth: + type: bearer + bearer: + token: jwt-token +vars: + pre-request: + - name: userId + value: '123' +scripts: + pre-request: // Pre-request script for GraphQL \ No newline at end of file diff --git a/packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/http-request.json b/packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/http-request.json new file mode 100644 index 000000000..438d21080 --- /dev/null +++ b/packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/http-request.json @@ -0,0 +1,70 @@ +{ + "name": "HTTP Methods", + "seq": 1, + "request": { + "method": "POST", + "url": "https://api.example.com/users", + "headers": [ + { + "name": "Content-Type", + "value": "application/json", + "description": "Content type header", + "enabled": true + }, + { + "name": "Accept", + "value": "application/json", + "enabled": false + } + ], + "params": [ + { + "name": "userId", + "value": "123", + "type": "path", + "description": "User ID parameter", + "enabled": true + }, + { + "name": "filter", + "value": "active", + "type": "query", + "enabled": false + } + ], + "body": { + "mode": "json", + "json": "{\n \"name\": \"John Doe\",\n \"email\": \"john@example.com\"\n}" + }, + "auth": { + "mode": "basic", + "basic": { + "username": "admin", + "password": "secret" + } + }, + "vars": { + "req": [ + { + "name": "userId", + "value": "123", + "description": "User ID variable", + "enabled": true + } + ], + "res": [ + { + "name": "token", + "value": "response.token", + "enabled": false + } + ] + }, + "script": { + "req": "// Pre-request script\nconsole.log('pre-request');", + "res": "// Post-response script\nconsole.log('post-response');" + }, + "tests": "// Test script\nassert.response.status === 200;", + "docs": "# User Creation API\nThis endpoint creates a new user." + } +} \ No newline at end of file diff --git a/packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/http-request.yml b/packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/http-request.yml new file mode 100644 index 000000000..41db2d376 --- /dev/null +++ b/packages/bruno-lang/v3/tests/stringifyRequest/__fixtures__/http-request.yml @@ -0,0 +1,58 @@ +meta: + name: HTTP Methods + seq: 1 +http: + method: post + url: https://api.example.com/users + params: + query: + - name: filter + value: active + type: query + disabled: true + path: + - name: userId + value: '123' + type: path + description: User ID parameter + headers: + - name: Content-Type + value: application/json + description: Content type header + - name: Accept + value: application/json + disabled: true + body: + type: json + data: |- + { + "name": "John Doe", + "email": "john@example.com" + } + auth: + type: basic + basic: + username: admin + password: secret +vars: + pre-request: + - name: userId + value: '123' + description: User ID variable + post-response: + - name: token + value: response.token + disabled: true +scripts: + pre-request: |- + // Pre-request script + console.log('pre-request'); + post-response: |- + // Post-response script + console.log('post-response'); +tests: |- + // Test script + assert.response.status === 200; +docs: |- + # User Creation API + This endpoint creates a new user. diff --git a/packages/bruno-lang/v3/tests/stringifyRequest/graphql.test.js b/packages/bruno-lang/v3/tests/stringifyRequest/graphql.test.js new file mode 100644 index 000000000..788ce851c --- /dev/null +++ b/packages/bruno-lang/v3/tests/stringifyRequest/graphql.test.js @@ -0,0 +1,103 @@ +const { stringifyRequest } = require('../../src'); +const yaml = require('js-yaml'); +const path = require('path'); +const fs = require('fs'); + +describe('GraphQL Request Handling', () => { + const loadFixture = (filename) => { + const filePath = path.join(__dirname, '__fixtures__', filename); + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + }; + + it('should generate exact YAML matching the fixture', () => { + const json = loadFixture('graphql-request.json'); + const expectedYaml = fs.readFileSync( + path.join(__dirname, '__fixtures__', 'graphql-request.yml'), + 'utf8' + ); + const generatedYaml = stringifyRequest(json); + + // Normalize line endings and whitespace for comparison + const normalizeString = (str) => str.replace(/\r\n/g, '\n').trim(); + expect(normalizeString(generatedYaml)).toBe(normalizeString(expectedYaml)); + }); + + it('should correctly format GraphQL request', () => { + const json = loadFixture('graphql-request.json'); + const result = yaml.load(stringifyRequest(json)); + + // Verify GraphQL section exists instead of HTTP + expect(result.graphql).toBeDefined(); + expect(result.http).toBeUndefined(); + + // Verify basic properties + expect(result.graphql.method).toBe('post'); + expect(result.graphql.url).toBe('https://api.example.com/graphql'); + + // Verify headers + expect(result.graphql.headers).toHaveLength(1); + expect(result.graphql.headers[0]).toEqual({ + name: 'Content-Type', + value: 'application/json' + }); + + // Verify body + expect(result.graphql.body).toEqual({ + type: 'graphql', + query: 'query GetUser($id: ID!) {\n user(id: $id) {\n id\n name\n email\n }\n}', + variables: '{\n \"id\": \"123\"\n}' + }); + + // Verify auth + expect(result.graphql.auth).toEqual({ + type: 'bearer', + bearer: { + token: 'jwt-token' + } + }); + + // Verify vars + expect(result.vars['pre-request']).toHaveLength(1); + expect(result.vars['pre-request'][0]).toEqual({ + name: 'userId', + value: '123' + }); + + // Verify scripts + expect(result.scripts['pre-request']).toBe('// Pre-request script for GraphQL'); + }); + + it('should handle GraphQL request without variables', () => { + const json = loadFixture('graphql-request.json'); + json.request.body.graphql.variables = ''; + + const result = yaml.load(stringifyRequest(json)); + expect(result.graphql.body.variables).toBe(''); + }); + + it('should handle GraphQL request without optional components', () => { + const json = loadFixture('graphql-request.json'); + + // Remove optional components + delete json.request.auth; + delete json.request.vars; + delete json.request.script; + + const result = yaml.load(stringifyRequest(json)); + + expect(result.graphql.auth).toEqual({ type: 'none' }); + expect(result.vars).toBeUndefined(); + expect(result.scripts).toBeUndefined(); + }); + + it('should detect GraphQL request based on body mode', () => { + const json = loadFixture('graphql-request.json'); + + // Change URL but keep GraphQL body + json.request.url = 'https://api.example.com/not-graphql'; + const result = yaml.load(stringifyRequest(json)); + + expect(result.graphql).toBeDefined(); + expect(result.http).toBeUndefined(); + }); +}); \ No newline at end of file diff --git a/packages/bruno-lang/v3/tests/stringifyRequest/http.test.js b/packages/bruno-lang/v3/tests/stringifyRequest/http.test.js new file mode 100644 index 000000000..e727a7d02 --- /dev/null +++ b/packages/bruno-lang/v3/tests/stringifyRequest/http.test.js @@ -0,0 +1,244 @@ +const { stringifyRequest } = require('../../src'); +const yaml = require('js-yaml'); +const path = require('path'); +const fs = require('fs'); + +describe('HTTP Request Handling', () => { + const loadFixture = (filename) => { + const filePath = path.join(__dirname, '__fixtures__', filename); + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + }; + + it('should generate exact YAML matching the fixture', () => { + const json = loadFixture('http-request.json'); + const expectedYaml = fs.readFileSync( + path.join(__dirname, '__fixtures__', 'http-request.yml'), + 'utf8' + ); + const generatedYaml = stringifyRequest(json); + + // Normalize line endings and whitespace for comparison + const normalizeString = (str) => str.replace(/\r\n/g, '\n').trim(); + expect(normalizeString(generatedYaml)).toBe(normalizeString(expectedYaml)); + }); + + it('should correctly format basic HTTP request', () => { + const json = loadFixture('http-request.json'); + const result = yaml.load(stringifyRequest(json)); + + // Verify HTTP section exists instead of GraphQL + expect(result.http).toBeDefined(); + expect(result.graphql).toBeUndefined(); + + // Verify basic properties + expect(result.meta.name).toBe('HTTP Methods'); + expect(result.meta.seq).toBe(1); + expect(result.http.method).toBe('post'); + expect(result.http.url).toBe('https://api.example.com/users'); + }); + + it('should handle request with query parameters', () => { + const json = loadFixture('http-request.json'); + const result = yaml.load(stringifyRequest(json)); + + expect(result.http.params.query).toEqual([ + { + name: 'filter', + value: 'active', + type: 'query', + disabled: true + } + ]); + }); + + it('should handle request with path parameters', () => { + const json = loadFixture('http-request.json'); + const result = yaml.load(stringifyRequest(json)); + + expect(result.http.params.path).toEqual([ + { + name: 'userId', + value: '123', + type: 'path', + description: 'User ID parameter' + } + ]); + }); + + it('should handle request with headers', () => { + const json = loadFixture('http-request.json'); + const result = yaml.load(stringifyRequest(json)); + + expect(result.http.headers).toEqual([ + { + name: 'Content-Type', + value: 'application/json', + description: 'Content type header' + }, + { + name: 'Accept', + value: 'application/json', + disabled: true + } + ]); + }); + + describe('Body Handling', () => { + it('should handle JSON body', () => { + const json = loadFixture('http-request.json'); + const result = yaml.load(stringifyRequest(json)); + + expect(result.http.body).toEqual({ + type: 'json', + data: '{\n "name": "John Doe",\n "email": "john@example.com"\n}' + }); + }); + + it('should handle form-urlencoded body', () => { + const json = loadFixture('http-request.json'); + json.request.body = { + mode: 'formUrlEncoded', + formUrlEncoded: [ + { name: 'username', value: 'johndoe', description: 'Username field', enabled: true }, + { name: 'password', value: 'secret', enabled: false } + ] + }; + const result = yaml.load(stringifyRequest(json)); + + expect(result.http.body).toEqual({ + type: 'form-urlencoded', + data: [ + { name: 'username', value: 'johndoe', description: 'Username field' }, + { name: 'password', value: 'secret', disabled: true } + ] + }); + }); + + it('should handle multipart-form body', () => { + const json = loadFixture('http-request.json'); + json.request.body = { + mode: 'multipartForm', + multipartForm: [ + { name: 'file', value: ['path/to/file'], type: 'file', enabled: true }, + { name: 'description', value: 'profile photo', type: 'text', enabled: true } + ] + }; + const result = yaml.load(stringifyRequest(json)); + + expect(result.http.body).toEqual({ + type: 'multipart-form', + data: [ + { name: 'file', value: ['path/to/file'], type: 'file' }, + { name: 'description', value: 'profile photo', type: 'text' } + ] + }); + }); + }); + + describe('Auth Handling', () => { + it('should handle basic auth', () => { + const json = loadFixture('http-request.json'); + const result = yaml.load(stringifyRequest(json)); + + expect(result.http.auth).toEqual({ + type: 'basic', + basic: { + username: 'admin', + password: 'secret' + } + }); + }); + + it('should handle bearer auth', () => { + const json = loadFixture('http-request.json'); + json.request.auth = { + mode: 'bearer', + bearer: { token: 'xyz123' } + }; + const result = yaml.load(stringifyRequest(json)); + + expect(result.http.auth).toEqual({ + type: 'bearer', + bearer: { + token: 'xyz123' + } + }); + }); + + it('should handle oauth2 password grant', () => { + const json = loadFixture('http-request.json'); + json.request.auth = { + mode: 'oauth2', + oauth2: { + grantType: 'password', + accessTokenUrl: 'https://api.example.com/oauth/token', + username: 'user', + password: 'pass', + clientId: 'client123', + clientSecret: 'secret123', + scope: 'read write' + } + }; + const result = yaml.load(stringifyRequest(json)); + + expect(result.http.auth).toEqual({ + type: 'oauth2', + oauth2: { + grant_type: 'password', + access_token_url: 'https://api.example.com/oauth/token', + username: 'user', + password: 'pass', + client_id: 'client123', + client_secret: 'secret123', + scope: 'read write' + } + }); + }); + }); + + describe('Variables and Scripts', () => { + it('should handle pre-request variables', () => { + const json = loadFixture('http-request.json'); + const result = yaml.load(stringifyRequest(json)); + + expect(result.vars['pre-request']).toEqual([ + { name: 'userId', value: '123', description: 'User ID variable' } + ]); + }); + + it('should handle post-response variables', () => { + const json = loadFixture('http-request.json'); + const result = yaml.load(stringifyRequest(json)); + + expect(result.vars['post-response']).toEqual([ + { name: 'token', value: 'response.token', disabled: true } + ]); + }); + + it('should handle scripts', () => { + const json = loadFixture('http-request.json'); + const result = yaml.load(stringifyRequest(json)); + + expect(result.scripts['pre-request']).toBe('// Pre-request script\nconsole.log(\'pre-request\');'); + expect(result.scripts['post-response']).toBe('// Post-response script\nconsole.log(\'post-response\');'); + }); + }); + + it('should handle tests and docs', () => { + const json = loadFixture('http-request.json'); + const result = yaml.load(stringifyRequest(json)); + + expect(result.tests).toBe('// Test script\nassert.response.status === 200;'); + expect(result.docs).toBe('# User Creation API\nThis endpoint creates a new user.'); + }); + + it('should handle disabled components', () => { + const json = loadFixture('http-request.json'); + json.request.headers[0].enabled = false; + json.request.params[0].enabled = false; + const result = yaml.load(stringifyRequest(json)); + + expect(result.http.headers[0].disabled).toBe(true); + expect(result.http.params.query[0].disabled).toBe(true); + }); +}); diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index b6e044ae4..b9db3a2f3 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -41,7 +41,7 @@ const varsSchema = Yup.object({ // todo // anoop(4 feb 2023) - nobody uses this, and it needs to be removed - local: Yup.boolean() + local: Yup.boolean().optional() }) .noUnknown(true) .strict(); @@ -298,9 +298,10 @@ const folderRootSchema = Yup.object({ const itemSchema = Yup.object({ uid: uidSchema, + name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'), + description: Yup.string().nullable(), type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder', 'js']).required('type is required'), seq: Yup.number().min(1), - name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'), request: requestSchema.when('type', { is: (type) => ['http-request', 'graphql-request'].includes(type), then: (schema) => schema.required('request is required when item-type is request')