From 6a85635c4982b624a4b1743d27b88a27756c6e07 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com> Date: Thu, 13 Mar 2025 00:49:57 +0530 Subject: [PATCH] Fix: Inconsistent JSON parsing and formatting in `res.body` and Res-preview (#4103) * Fix: Revert selective JSON parsing where string response is not parsed - Revert "Merge pull request #3706 from Pragadesh-45/fix/response-format-updates" - e897dc1eb0acb16be6249e51efb2cc7c52cede47 - Revert "Merge pull request #3676 from pooja-bruno/fix/string-json-response" - 1f2bee1f90b3641c69e608fc579f2f4dd6439584 * Fix: Revert interpreting Assert RHS-value wrapped in quotes literally - Revert "Merge pull request #3806 from Pragadesh-45/fix/handle-assert-results" - 63d3cb380d02e56753ff9926b5789533d7ddac1f - Revert "Merge pull request #3805 from Pragadesh-45/fix/handle-assert-results" - 6abd063749926fc66e57b1bdbcabbf2e43ec29ac * Fix: Inconsistent JSON formatting in preview when encoded value is a string * Fix: Prettify JSON for Res-preview without parsing to avoid JS specific roundings * Fix(testbench): req.body is always Buffer after the binary req body related changes * Added `/api/echo/custom` where response can be configured using request itself * Added tests for validating Assert and Response-preview Co-authored-by: Pragadesh-45 * Handle char-encoding in Response-preview and added more tests * Updated API endpoint in tests to use httpfaker api * QuickJS (Safe Mode) exec logic to handle template literals similar to Developer Mode * Safe Mode bru.runRequest to return statusText similar to Developer Mode --------- Co-authored-by: ramki-bruno Co-authored-by: Anoop M D --- package-lock.json | 1 + packages/bruno-app/package.json | 1 + .../ResponsePane/QueryResult/index.js | 38 +++++----- packages/bruno-app/src/utils/common/index.js | 14 ++++ packages/bruno-cli/src/utils/common.js | 8 +- .../bruno-electron/src/ipc/network/index.js | 8 +- .../bruno-js/src/sandbox/quickjs/index.js | 42 ++++++----- .../bruno-js/src/sandbox/quickjs/shims/bru.js | 4 +- packages/bruno-js/src/utils.js | 8 ++ .../asserts/test-assert-combinations.bru | 74 +++++++++++++++++++ .../collection/echo/test echo any.bru | 22 ++++++ .../collection/echo/test echo-any json.bru | 22 ++++++ .../collection/environments/Local.bru | 7 ++ .../collection/environments/Prod.bru | 1 + .../test JSON false response.bru | 22 ++++++ .../test JSON null response.bru | 22 ++++++ .../test JSON number response.bru | 22 ++++++ .../response-parsing/test JSON response.bru | 22 ++++++ .../test JSON string response.bru | 22 ++++++ .../test JSON string with quotes response.bru | 22 ++++++ .../test JSON true response.bru | 22 ++++++ .../test JSON unsafe-int response.bru | 26 +++++++ .../response-parsing/test binary response.bru | 34 +++++++++ .../response-parsing/test html response.bru | 22 ++++++ .../response-parsing/test image response.bru | 18 +++++ ... invalid JSON response with formatting.bru | 22 ++++++ ...st plain text response with formatting.bru | 22 ++++++ .../test plain text response.bru | 23 ++++++ .../test plain text utf16 response.bru | 22 ++++++ ... plain text utf16-be with BOM response.bru | 22 ++++++ ... plain text utf16-le with BOM response.bru | 22 ++++++ ...test plain text utf8 with BOM response.bru | 22 ++++++ .../response-parsing/test xml response.bru | 22 ++++++ packages/bruno-tests/src/echo/index.js | 26 +++++++ packages/bruno-tests/src/index.js | 15 +++- 35 files changed, 669 insertions(+), 53 deletions(-) create mode 100644 packages/bruno-tests/collection/asserts/test-assert-combinations.bru create mode 100644 packages/bruno-tests/collection/echo/test echo any.bru create mode 100644 packages/bruno-tests/collection/echo/test echo-any json.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test JSON false response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test JSON null response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test JSON number response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test JSON response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test JSON string response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test JSON string with quotes response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test JSON true response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test JSON unsafe-int response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test binary response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test html response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test image response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test invalid JSON response with formatting.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test plain text response with formatting.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test plain text response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test plain text utf16 response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test plain text utf16-be with BOM response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test plain text utf16-le with BOM response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test plain text utf8 with BOM response.bru create mode 100644 packages/bruno-tests/collection/response-parsing/test xml response.bru diff --git a/package-lock.json b/package-lock.json index 6e5aa5ec4..6b7c83faa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24447,6 +24447,7 @@ "graphql-request": "^3.7.0", "httpsnippet": "^3.0.9", "i18next": "24.1.2", + "iconv-lite": "^0.6.3", "idb": "^7.0.0", "immer": "^9.0.15", "jsesc": "^3.0.2", diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index 224bb24df..3ce187801 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -36,6 +36,7 @@ "graphql-request": "^3.7.0", "httpsnippet": "^3.0.9", "i18next": "24.1.2", + "iconv-lite": "^0.6.3", "idb": "^7.0.0", "immer": "^9.0.15", "jsesc": "^3.0.2", diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js index c1631a358..78993a413 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js @@ -3,45 +3,49 @@ import QueryResultFilter from './QueryResultFilter'; import { JSONPath } from 'jsonpath-plus'; import React from 'react'; import classnames from 'classnames'; +import iconv from 'iconv-lite'; import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common'; import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror'; import QueryResultPreview from './QueryResultPreview'; import StyledWrapper from './StyledWrapper'; import { useState, useMemo, useEffect } from 'react'; import { useTheme } from 'providers/Theme/index'; -import { uuid } from 'utils/common/index'; +import { getEncoding, prettifyJson, uuid } from 'utils/common/index'; -const formatResponse = (data, mode, filter) => { - if (data === undefined) { +const formatResponse = (data, dataBuffer, encoding, mode, filter) => { + if (data === undefined || !dataBuffer) { return ''; } - if (data === null) { - return 'null'; - } + // TODO: We need a better way to get the raw response-data here instead + // of using this dataBuffer param. + // Also, we only need the raw response-data and content-type to show the preview. + const rawData = iconv.decode( + Buffer.from(dataBuffer, "base64"), + iconv.encodingExists(encoding) ? encoding : "utf-8" + ); if (mode.includes('json')) { - let isValidJSON = false; - try { - isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object' + JSON.parse(rawData); } catch (error) { - console.log('Error parsing JSON: ', error.message); - } - - if (!isValidJSON && typeof data === 'string') { - return data; + // If the response content-type is JSON and it fails parsing, its an invalid JSON. + // In that case, just show the response as it is in the preview. + return rawData; } if (filter) { try { data = JSONPath({ path: filter, json: data }); + return prettifyJson(JSON.stringify(data)); } catch (e) { console.warn('Could not apply JSONPath filter:', e.message); } } - return safeStringifyJSON(data, true); + // Prettify the JSON string directly instead of parse->stringify to avoid + // issues like rounding numbers bigger than Number.MAX_SAFE_INTEGER etc. + return prettifyJson(rawData); } if (mode.includes('xml')) { @@ -56,7 +60,7 @@ const formatResponse = (data, mode, filter) => { return data; } - return safeStringifyJSON(data, true); + return prettifyJson(rawData); }; const formatErrorMessage = (error) => { @@ -76,7 +80,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven const contentType = getContentType(headers); const mode = getCodeMirrorModeBasedOnContentType(contentType, data); const [filter, setFilter] = useState(null); - const formattedData = formatResponse(data, mode, filter); + const formattedData = formatResponse(data, dataBuffer, getEncoding(headers), mode, filter); const { displayedTheme } = useTheme(); const debouncedResultFilterOnChange = debounce((e) => { diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js index f9f1c7fbe..518b1908b 100644 --- a/packages/bruno-app/src/utils/common/index.js +++ b/packages/bruno-app/src/utils/common/index.js @@ -1,5 +1,6 @@ import { customAlphabet } from 'nanoid'; import xmlFormat from 'xml-formatter'; +import { format as jsoncFormat, applyEdits as jsoncApplyEdits } from 'jsonc-parser'; // a customized version of nanoid without using _ and - export const uuid = () => { @@ -26,6 +27,13 @@ export const waitForNextTick = () => { }); }; +export const prettifyJson = (doc) => { + return jsoncApplyEdits( + doc, + jsoncFormat(doc, null, {insertSpaces: true, tabSize: 2}) + ); +} + export const safeParseJSON = (str) => { if (!str || !str.length || typeof str !== 'string') { return str; @@ -176,3 +184,9 @@ export const generateUidBasedOnHash = (str) => { }; export const stringifyIfNot = v => typeof v === 'string' ? v : String(v); + +export const getEncoding = (headers) => { + // Parse the charset from content type: https://stackoverflow.com/a/33192813 + const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(headers['content-type'] || ''); + return charsetMatch?.[1]; +} diff --git a/packages/bruno-cli/src/utils/common.js b/packages/bruno-cli/src/utils/common.js index d2eeec366..7a2ae3bf2 100644 --- a/packages/bruno-cli/src/utils/common.js +++ b/packages/bruno-cli/src/utils/common.js @@ -34,14 +34,10 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) => // Filter out ZWNBSP character // https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d data = data.replace(/^\uFEFF/, ''); - - // If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed - if (!disableParsingResponseJson && !(typeof data === 'string' && data.startsWith('"') && data.endsWith('"'))) { + if (!disableParsingResponseJson) { data = JSON.parse(data); } - } catch { - - } + } catch { } return { data, dataBuffer }; }; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 50b234ca5..bc71aac72 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -398,14 +398,10 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) => // Filter out ZWNBSP character // https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d data = data.replace(/^\uFEFF/, ''); - - // If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed - if ( !disableParsingResponseJson && ! (typeof data === 'string' && data.startsWith("\"") && data.endsWith("\""))) { + if (!disableParsingResponseJson) { data = JSON.parse(data); } - } catch { - console.log('Failed to parse response data as JSON'); - } + } catch { } return { data, dataBuffer }; }; diff --git a/packages/bruno-js/src/sandbox/quickjs/index.js b/packages/bruno-js/src/sandbox/quickjs/index.js index 58ccd885d..2c83c0e3f 100644 --- a/packages/bruno-js/src/sandbox/quickjs/index.js +++ b/packages/bruno-js/src/sandbox/quickjs/index.js @@ -22,6 +22,12 @@ const toNumber = (value) => { return Number.isInteger(num) ? parseInt(value, 10) : parseFloat(value); }; +const removeQuotes = (str) => { + if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) { + return str.slice(1, -1); + } + return str; +}; const executeQuickJsVm = ({ script: externalScript, context: externalContext, scriptType = 'template-literal' }) => { if (!externalScript?.length || typeof externalScript !== 'string') { @@ -29,16 +35,26 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc } externalScript = externalScript?.trim(); - if (!isNaN(Number(externalScript))) { - return Number(externalScript); + if(scriptType === 'template-literal') { + if (!isNaN(Number(externalScript))) { + const number = Number(externalScript); + + // Check if the number is too high. Too high number might get altered, see #1000 + if (number > Number.MAX_SAFE_INTEGER) { + return externalScript; + } + + return toNumber(externalScript); + } + + if (externalScript === 'true') return true; + if (externalScript === 'false') return false; + if (externalScript === 'null') return null; + if (externalScript === 'undefined') return undefined; + + externalScript = removeQuotes(externalScript); } - if (externalScript === 'true') return true; - if (externalScript === 'false') return false; - if (externalScript === 'null') return null; - if (externalScript === 'undefined') return undefined; - - const vm = QuickJSSyncContext; try { @@ -78,16 +94,6 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external } externalScript = externalScript?.trim(); - if (!isNaN(Number(externalScript))) { - return toNumber(externalScript); - } - - if (externalScript === 'true') return true; - if (externalScript === 'false') return false; - if (externalScript === 'null') return null; - if (externalScript === 'undefined') return undefined; - - try { const module = await newQuickJSWASMModule(); const vm = module.newContext(); diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js index fa4cbf341..4ccd8ebfc 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js @@ -189,8 +189,8 @@ const addBruShimToContext = (vm, bru) => { const promise = vm.newPromise(); bru.runRequest(vm.dump(args)) .then((response) => { - const { status, headers, data, dataBuffer, size } = response || {}; - promise.resolve(marshallToVm(cleanJson({ status, headers, data, dataBuffer, size }), vm)); + const { status, statusText, headers, data, dataBuffer, size } = response || {}; + promise.resolve(marshallToVm(cleanJson({ status, statusText, headers, data, dataBuffer, size }), vm)); }) .catch((err) => { promise.resolve( diff --git a/packages/bruno-js/src/utils.js b/packages/bruno-js/src/utils.js index 6b5ecacb5..55b454d02 100644 --- a/packages/bruno-js/src/utils.js +++ b/packages/bruno-js/src/utils.js @@ -85,6 +85,14 @@ const evaluateJsTemplateLiteral = (templateLiteral, context) => { return undefined; } + if (templateLiteral.startsWith('"') && templateLiteral.endsWith('"')) { + return templateLiteral.slice(1, -1); + } + + if (templateLiteral.startsWith("'") && templateLiteral.endsWith("'")) { + return templateLiteral.slice(1, -1); + } + if (!isNaN(templateLiteral)) { const number = Number(templateLiteral); // Check if the number is too high. Too high number might get altered, see #1000 diff --git a/packages/bruno-tests/collection/asserts/test-assert-combinations.bru b/packages/bruno-tests/collection/asserts/test-assert-combinations.bru new file mode 100644 index 000000000..3ad85765a --- /dev/null +++ b/packages/bruno-tests/collection/asserts/test-assert-combinations.bru @@ -0,0 +1,74 @@ +meta { + name: test-assert-combinations + type: http + seq: 1 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "type": "application/json", + "contentJSON": { + "string": "foo", + "stringWithSQuotes": "'foo'", + "stringWithDQuotes": "\"foo\"", + "number": 123, + "numberAsString": "123", + "numberAsStringWithSQuotes": "'123'", + "numberAsStringWithDQuotes": "\"123\"", + "numberAsStringWithLeadingZero": "0123", + "numberBig": 9007199254740992000, + "numberBigAsString": "9007199254740991999", + "null": null, + "nullAsString": "null", + "nullAsStringWithSQuotes": "'null'", + "nullAsStringWithDQuotes": "\"null\"", + "true": true, + "trueAsString": "true", + "trueAsStringWithSQuotes": "'true'", + "trueAsStringWithDQuotes": "\"true\"", + "false": false, + "falseAsString": "false", + "falseAsStringWithSQuotes": "'false'", + "falseAsStringWithDQuotes": "\"false\"", + "stringWithCurlyBraces": "{foo}", + "stringWithDoubleCurlyBraces": "{{foobar}}" + } + } +} + +assert { + res.body.string: eq foo + res.body.string: eq 'foo' + res.body.string: eq "foo" + res.body.stringWithSQuotes: eq "'foo'" + res.body.stringWithDQuotes: eq '"foo"' + res.body.number: eq 123 + res.body.numberAsString: eq '123' + res.body.numberAsString: eq "123" + res.body.numberAsStringWithSQuotes: eq "'123'" + res.body.numberAsStringWithDQuotes: eq '"123"' + res.body.numberAsStringWithLeadingZero: eq "0123" + res.body.numberBig.toString(): eq '9007199254740992000' + res.body.numberBigAsString: eq "9007199254740991999" + res.body.null: eq null + res.body.nullAsString: eq "null" + res.body.nullAsStringWithSQuotes: eq "'null'" + res.body.nullAsStringWithDQuotes: eq '"null"' + res.body.true: eq true + res.body.trueAsString: eq "true" + res.body.trueAsStringWithSQuotes: eq "'true'" + res.body.trueAsStringWithDQuotes: eq '"true"' + res.body.false: eq false + res.body.falseAsString: eq "false" + res.body.falseAsStringWithSQuotes: eq "'false'" + res.body.falseAsStringWithDQuotes: eq '"false"' + res.body.nonexistent: eq undefined + res.body.stringWithCurlyBraces: eq "{foo}" + res.body.stringWithDoubleCurlyBraces: eq "{{foobar}}" +} diff --git a/packages/bruno-tests/collection/echo/test echo any.bru b/packages/bruno-tests/collection/echo/test echo any.bru new file mode 100644 index 000000000..b78014dd9 --- /dev/null +++ b/packages/bruno-tests/collection/echo/test echo any.bru @@ -0,0 +1,22 @@ +meta { + name: test echo any + type: http + seq: 11 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "text/plain" }, + "content": "hello" + } +} + +assert { + res.body: eq hello +} diff --git a/packages/bruno-tests/collection/echo/test echo-any json.bru b/packages/bruno-tests/collection/echo/test echo-any json.bru new file mode 100644 index 000000000..2f3a7e5f4 --- /dev/null +++ b/packages/bruno-tests/collection/echo/test echo-any json.bru @@ -0,0 +1,22 @@ +meta { + name: test echo-any json + type: http + seq: 12 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "type": "application/json", + "contentJSON": {"x": 42} + } +} + +assert { + res.body.x: eq 42 +} diff --git a/packages/bruno-tests/collection/environments/Local.bru b/packages/bruno-tests/collection/environments/Local.bru index 991077d97..a7ac3f541 100644 --- a/packages/bruno-tests/collection/environments/Local.bru +++ b/packages/bruno-tests/collection/environments/Local.bru @@ -1,7 +1,14 @@ vars { host: http://localhost:8080 + httpfaker: https://www.httpfaker.org bearer_auth_token: your_secret_token basic_auth_password: della + env.var1: envVar1 + env-var2: envVar2 + bark: {{process.env.PROC_ENV_VAR}} + foo: bar + testSetEnvVar: bruno-29653 + echo-host: https://echo.usebruno.com client_id: client_id_1 client_secret: client_secret_1 auth_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize diff --git a/packages/bruno-tests/collection/environments/Prod.bru b/packages/bruno-tests/collection/environments/Prod.bru index ce8fa60cc..f33c1bb05 100644 --- a/packages/bruno-tests/collection/environments/Prod.bru +++ b/packages/bruno-tests/collection/environments/Prod.bru @@ -1,5 +1,6 @@ vars { host: https://testbench-sanity.usebruno.com + httpfaker: https://www.httpfaker.org bearer_auth_token: your_secret_token basic_auth_password: della env.var1: envVar1 diff --git a/packages/bruno-tests/collection/response-parsing/test JSON false response.bru b/packages/bruno-tests/collection/response-parsing/test JSON false response.bru new file mode 100644 index 000000000..a507434e5 --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test JSON false response.bru @@ -0,0 +1,22 @@ +meta { + name: test JSON false response + type: http + seq: 11 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "application/json" }, + "content": "false" + } +} + +assert { + res.body: eq false +} diff --git a/packages/bruno-tests/collection/response-parsing/test JSON null response.bru b/packages/bruno-tests/collection/response-parsing/test JSON null response.bru new file mode 100644 index 000000000..156aa7fbe --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test JSON null response.bru @@ -0,0 +1,22 @@ +meta { + name: test JSON null response + type: http + seq: 6 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "application/json" }, + "content": "null" + } +} + +assert { + res.body: eq null +} diff --git a/packages/bruno-tests/collection/response-parsing/test JSON number response.bru b/packages/bruno-tests/collection/response-parsing/test JSON number response.bru new file mode 100644 index 000000000..8f995b395 --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test JSON number response.bru @@ -0,0 +1,22 @@ +meta { + name: test JSON number response + type: http + seq: 12 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "application/json" }, + "content": "3.1" + } +} + +assert { + res.body: eq 3.1 +} diff --git a/packages/bruno-tests/collection/response-parsing/test JSON response.bru b/packages/bruno-tests/collection/response-parsing/test JSON response.bru new file mode 100644 index 000000000..018cce86d --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test JSON response.bru @@ -0,0 +1,22 @@ +meta { + name: test JSON response + type: http + seq: 2 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "application/json" }, + "contentJSON": { "message": "hello" } + } +} + +assert { + res.body.message: eq hello +} diff --git a/packages/bruno-tests/collection/response-parsing/test JSON string response.bru b/packages/bruno-tests/collection/response-parsing/test JSON string response.bru new file mode 100644 index 000000000..18a0e3909 --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test JSON string response.bru @@ -0,0 +1,22 @@ +meta { + name: test JSON string response + type: http + seq: 7 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "application/json" }, + "content": "\"ok\"" + } +} + +assert { + res.body: eq ok +} diff --git a/packages/bruno-tests/collection/response-parsing/test JSON string with quotes response.bru b/packages/bruno-tests/collection/response-parsing/test JSON string with quotes response.bru new file mode 100644 index 000000000..d262ff084 --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test JSON string with quotes response.bru @@ -0,0 +1,22 @@ +meta { + name: test JSON string with quotes response + type: http + seq: 8 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "application/json" }, + "contentJSON": "\"ok\"" + } +} + +assert { + res.body: eq '"ok"' +} diff --git a/packages/bruno-tests/collection/response-parsing/test JSON true response.bru b/packages/bruno-tests/collection/response-parsing/test JSON true response.bru new file mode 100644 index 000000000..7f4c0bd65 --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test JSON true response.bru @@ -0,0 +1,22 @@ +meta { + name: test JSON true response + type: http + seq: 10 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "application/json" }, + "content": "true" + } +} + +assert { + res.body: eq true +} diff --git a/packages/bruno-tests/collection/response-parsing/test JSON unsafe-int response.bru b/packages/bruno-tests/collection/response-parsing/test JSON unsafe-int response.bru new file mode 100644 index 000000000..db4fe3bcc --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test JSON unsafe-int response.bru @@ -0,0 +1,26 @@ +meta { + name: test JSON unsafe-int response + type: http + seq: 13 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "application/json" }, + "content": "90071992547409919876" + } +} + +assert { + res.body.toString(): eq 90071992547409920000 +} + +docs { + Note: This test is not perfect, we should match the unparsed raw-response with the expected string version of the unsafe-integer +} diff --git a/packages/bruno-tests/collection/response-parsing/test binary response.bru b/packages/bruno-tests/collection/response-parsing/test binary response.bru new file mode 100644 index 000000000..53e6e3436 --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test binary response.bru @@ -0,0 +1,34 @@ +meta { + name: test binary response + type: http + seq: 4 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "type": "application/octet-stream", + "contentBase64": "+Z1P82iH1wmbILfvnhvjQVbVAktP4TzltpxYD74zNyA=" + } +} + +tests { + test("response matches the expectation after utf-8 decoding(needs improvement)", function () { + expect(res.getStatus()).to.equal(200); + const dataBinary = Buffer.from("+Z1P82iH1wmbILfvnhvjQVbVAktP4TzltpxYD74zNyA=", "base64"); + expect(res.body).to.equal(dataBinary.toString("utf-8")); + }); +} + +docs { + Note: + + This test is not perfect and needs to be improved by direclty matching expected binary data with raw-response. + + Currently res.body is decoded with `utf-8` by default and looses data in the process. We need some property in `res` which gives access to raw-data/Buffer. +} diff --git a/packages/bruno-tests/collection/response-parsing/test html response.bru b/packages/bruno-tests/collection/response-parsing/test html response.bru new file mode 100644 index 000000000..48bf15310 --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test html response.bru @@ -0,0 +1,22 @@ +meta { + name: test html response + type: http + seq: 5 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "text/html" }, + "content": "

hello

" + } +} + +assert { + res.body: eq

hello

+} diff --git a/packages/bruno-tests/collection/response-parsing/test image response.bru b/packages/bruno-tests/collection/response-parsing/test image response.bru new file mode 100644 index 000000000..4ca65adab --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test image response.bru @@ -0,0 +1,18 @@ +meta { + name: test image response + type: http + seq: 3 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "type": "image/png", + "contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkAQMAAABKLAcXAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGUExURQCqAP///59OGOoAAAABYktHRAH/Ai3eAAAAB3RJTUUH6QMHCwUNKHvFmgAAABRJREFUOMtjYBgFo2AUjIJRQE8AAAV4AAEpcbn8AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI1LTAzLTA3VDExOjA1OjEzKzAwOjAwQkgGWgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNS0wMy0wN1QxMTowNToxMyswMDowMDMVvuYAAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjUtMDMtMDdUMTE6MDU6MTMrMDA6MDBkAJ85AAAAAElFTkSuQmCC" + } +} diff --git a/packages/bruno-tests/collection/response-parsing/test invalid JSON response with formatting.bru b/packages/bruno-tests/collection/response-parsing/test invalid JSON response with formatting.bru new file mode 100644 index 000000000..57e2a6872 --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test invalid JSON response with formatting.bru @@ -0,0 +1,22 @@ +meta { + name: test invalid JSON response with formatting + type: http + seq: 19 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "application/json" }, + "content": "hello\n\tworld" + } +} + +assert { + res.body: eq hello\n\tworld +} diff --git a/packages/bruno-tests/collection/response-parsing/test plain text response with formatting.bru b/packages/bruno-tests/collection/response-parsing/test plain text response with formatting.bru new file mode 100644 index 000000000..af9a87144 --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test plain text response with formatting.bru @@ -0,0 +1,22 @@ +meta { + name: test plain text response with formatting + type: http + seq: 18 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "text/plain" }, + "content": "hello\n\tworld" + } +} + +assert { + res.body: eq hello\n\tworld +} diff --git a/packages/bruno-tests/collection/response-parsing/test plain text response.bru b/packages/bruno-tests/collection/response-parsing/test plain text response.bru new file mode 100644 index 000000000..fbb884483 --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test plain text response.bru @@ -0,0 +1,23 @@ +meta { + name: test plain text response + type: http + seq: 1 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "text/plain" }, + "content": "hello" + } +} + +assert { + res.body: eq hello +} + diff --git a/packages/bruno-tests/collection/response-parsing/test plain text utf16 response.bru b/packages/bruno-tests/collection/response-parsing/test plain text utf16 response.bru new file mode 100644 index 000000000..096985f0a --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test plain text utf16 response.bru @@ -0,0 +1,22 @@ +meta { + name: test plain text utf16 response + type: http + seq: 14 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "text/plain; charset=utf-16" }, + "contentBase64": "dABoAGkAcwAgAGkAcwAgAGUAbgBjAG8AZABlAGQAIAB3AGkAdABoACAAdQB0AGYAMQA2AA==" + } +} + +assert { + res.body: eq "this is encoded with utf16" +} diff --git a/packages/bruno-tests/collection/response-parsing/test plain text utf16-be with BOM response.bru b/packages/bruno-tests/collection/response-parsing/test plain text utf16-be with BOM response.bru new file mode 100644 index 000000000..cffe54a97 --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test plain text utf16-be with BOM response.bru @@ -0,0 +1,22 @@ +meta { + name: test plain text utf16-be with BOM response + type: http + seq: 15 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "text/plain; charset=utf-16" }, + "contentBase64": "/v8AdABoAGkAcwAgAGkAcwAgAGUAbgBjAG8AZABlAGQAIAB3AGkAdABoACAAdQB0AGYAMQA2AC0AYgBlACAAdwBpAHQAaAAgAEIATwBN" + } +} + +assert { + res.body: eq "this is encoded with utf16-be with BOM" +} diff --git a/packages/bruno-tests/collection/response-parsing/test plain text utf16-le with BOM response.bru b/packages/bruno-tests/collection/response-parsing/test plain text utf16-le with BOM response.bru new file mode 100644 index 000000000..5d29a2701 --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test plain text utf16-le with BOM response.bru @@ -0,0 +1,22 @@ +meta { + name: test plain text utf16-le with BOM response + type: http + seq: 16 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "text/plain; charset=utf-16" }, + "contentBase64": "//50AGgAaQBzACAAaQBzACAAZQBuAGMAbwBkAGUAZAAgAHcAaQB0AGgAIAB1AHQAZgAxADYALQBsAGUAIAB3AGkAdABoACAAQgBPAE0A" + } +} + +assert { + res.body: eq "this is encoded with utf16-le with BOM" +} diff --git a/packages/bruno-tests/collection/response-parsing/test plain text utf8 with BOM response.bru b/packages/bruno-tests/collection/response-parsing/test plain text utf8 with BOM response.bru new file mode 100644 index 000000000..055386d79 --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test plain text utf8 with BOM response.bru @@ -0,0 +1,22 @@ +meta { + name: test plain text utf8 with BOM response + type: http + seq: 17 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "text/plain; charset=utf8" }, + "contentBase64": "77u/dGhpcyBpcyB1dGY4IGVuY29kZWQgd2l0aCBCT00sIHdoeSBub3Q/" + } +} + +assert { + res.body: eq "this is utf8 encoded with BOM, why not?" +} diff --git a/packages/bruno-tests/collection/response-parsing/test xml response.bru b/packages/bruno-tests/collection/response-parsing/test xml response.bru new file mode 100644 index 000000000..5e562ada2 --- /dev/null +++ b/packages/bruno-tests/collection/response-parsing/test xml response.bru @@ -0,0 +1,22 @@ +meta { + name: test xml response + type: http + seq: 9 +} + +post { + url: {{httpfaker}}/api/echo/custom + body: json + auth: none +} + +body:json { + { + "headers": { "content-type": "application/xml" }, + "content": "hello" + } +} + +assert { + res.body: eq hello +} diff --git a/packages/bruno-tests/src/echo/index.js b/packages/bruno-tests/src/echo/index.js index 89a9208e0..00b50bd36 100644 --- a/packages/bruno-tests/src/echo/index.js +++ b/packages/bruno-tests/src/echo/index.js @@ -48,4 +48,30 @@ router.get('/iso-enc', (req, res) => { return res.send(Buffer.from(responseText, 'latin1')); }); +router.post("/custom", (req, res) => { + const { headers, content, contentBase64, contentJSON, type } = req.body || {}; + + res._headers = {}; + + if (type) { + res.setHeader('Content-Type', type); + } + + if (headers && typeof headers === 'object') { + Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value); + }); + } + + if (contentBase64) { + res.write(Buffer.from(contentBase64, 'base64')); + } else if (contentJSON !== undefined) { + res.write(JSON.stringify(contentJSON)); + } else if (content !== undefined) { + res.write(content); + } + + return res.end(); +}); + module.exports = router; diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js index a482fc128..901cdba57 100644 --- a/packages/bruno-tests/src/index.js +++ b/packages/bruno-tests/src/index.js @@ -10,12 +10,19 @@ const multipartRouter = require('./multipart'); const app = new express(); const port = process.env.PORT || 8080; -app.use(express.raw({type: '*/*', limit: '100mb'})); app.use(cors()); + +const saveRawBody = (req, res, buf) => { + req.rawBuffer = Buffer.from(buf); + req.rawBody = buf.toString(); +}; + +app.use(bodyParser.json({ verify: saveRawBody })); +app.use(bodyParser.urlencoded({ extended: true, verify: saveRawBody })); +app.use(bodyParser.text({ verify: saveRawBody })); app.use(xmlParser()); -app.use(bodyParser.text()); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: true })); +app.use(express.raw({ type: '*/*', limit: '100mb', verify: saveRawBody })); + formDataParser.init(app, express); app.use('/api/auth', authRouter);