mirror of
https://github.com/usebruno/bruno.git
synced 2025-08-18 22:58:08 +02:00
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" -e897dc1eb0
- Revert "Merge pull request #3676 from pooja-bruno/fix/string-json-response" -1f2bee1f90
* Fix: Revert interpreting Assert RHS-value wrapped in quotes literally - Revert "Merge pull request #3806 from Pragadesh-45/fix/handle-assert-results" -63d3cb380d
- Revert "Merge pull request #3805 from Pragadesh-45/fix/handle-assert-results" -6abd063749
* 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 <temporaryg7904@gmail.com> * 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 <ramki@usebruno.com> Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -24447,6 +24447,7 @@
|
|||||||
"graphql-request": "^3.7.0",
|
"graphql-request": "^3.7.0",
|
||||||
"httpsnippet": "^3.0.9",
|
"httpsnippet": "^3.0.9",
|
||||||
"i18next": "24.1.2",
|
"i18next": "24.1.2",
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
"idb": "^7.0.0",
|
"idb": "^7.0.0",
|
||||||
"immer": "^9.0.15",
|
"immer": "^9.0.15",
|
||||||
"jsesc": "^3.0.2",
|
"jsesc": "^3.0.2",
|
||||||
|
@@ -36,6 +36,7 @@
|
|||||||
"graphql-request": "^3.7.0",
|
"graphql-request": "^3.7.0",
|
||||||
"httpsnippet": "^3.0.9",
|
"httpsnippet": "^3.0.9",
|
||||||
"i18next": "24.1.2",
|
"i18next": "24.1.2",
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
"idb": "^7.0.0",
|
"idb": "^7.0.0",
|
||||||
"immer": "^9.0.15",
|
"immer": "^9.0.15",
|
||||||
"jsesc": "^3.0.2",
|
"jsesc": "^3.0.2",
|
||||||
|
@@ -3,45 +3,49 @@ import QueryResultFilter from './QueryResultFilter';
|
|||||||
import { JSONPath } from 'jsonpath-plus';
|
import { JSONPath } from 'jsonpath-plus';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import iconv from 'iconv-lite';
|
||||||
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
|
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
|
||||||
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
|
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
|
||||||
import QueryResultPreview from './QueryResultPreview';
|
import QueryResultPreview from './QueryResultPreview';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import { useState, useMemo, useEffect } from 'react';
|
import { useState, useMemo, useEffect } from 'react';
|
||||||
import { useTheme } from 'providers/Theme/index';
|
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) => {
|
const formatResponse = (data, dataBuffer, encoding, mode, filter) => {
|
||||||
if (data === undefined) {
|
if (data === undefined || !dataBuffer) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data === null) {
|
// TODO: We need a better way to get the raw response-data here instead
|
||||||
return 'null';
|
// 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')) {
|
if (mode.includes('json')) {
|
||||||
let isValidJSON = false;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object'
|
JSON.parse(rawData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error parsing JSON: ', error.message);
|
// 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 (!isValidJSON && typeof data === 'string') {
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
try {
|
try {
|
||||||
data = JSONPath({ path: filter, json: data });
|
data = JSONPath({ path: filter, json: data });
|
||||||
|
return prettifyJson(JSON.stringify(data));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Could not apply JSONPath filter:', e.message);
|
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')) {
|
if (mode.includes('xml')) {
|
||||||
@@ -56,7 +60,7 @@ const formatResponse = (data, mode, filter) => {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return safeStringifyJSON(data, true);
|
return prettifyJson(rawData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatErrorMessage = (error) => {
|
const formatErrorMessage = (error) => {
|
||||||
@@ -76,7 +80,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
|||||||
const contentType = getContentType(headers);
|
const contentType = getContentType(headers);
|
||||||
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
|
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
|
||||||
const [filter, setFilter] = useState(null);
|
const [filter, setFilter] = useState(null);
|
||||||
const formattedData = formatResponse(data, mode, filter);
|
const formattedData = formatResponse(data, dataBuffer, getEncoding(headers), mode, filter);
|
||||||
const { displayedTheme } = useTheme();
|
const { displayedTheme } = useTheme();
|
||||||
|
|
||||||
const debouncedResultFilterOnChange = debounce((e) => {
|
const debouncedResultFilterOnChange = debounce((e) => {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
import xmlFormat from 'xml-formatter';
|
import xmlFormat from 'xml-formatter';
|
||||||
|
import { format as jsoncFormat, applyEdits as jsoncApplyEdits } from 'jsonc-parser';
|
||||||
|
|
||||||
// a customized version of nanoid without using _ and -
|
// a customized version of nanoid without using _ and -
|
||||||
export const uuid = () => {
|
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) => {
|
export const safeParseJSON = (str) => {
|
||||||
if (!str || !str.length || typeof str !== 'string') {
|
if (!str || !str.length || typeof str !== 'string') {
|
||||||
return str;
|
return str;
|
||||||
@@ -176,3 +184,9 @@ export const generateUidBasedOnHash = (str) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const stringifyIfNot = v => typeof v === 'string' ? v : String(v);
|
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];
|
||||||
|
}
|
||||||
|
@@ -34,14 +34,10 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
|
|||||||
// Filter out ZWNBSP character
|
// Filter out ZWNBSP character
|
||||||
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
|
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
|
||||||
data = data.replace(/^\uFEFF/, '');
|
data = data.replace(/^\uFEFF/, '');
|
||||||
|
if (!disableParsingResponseJson) {
|
||||||
// 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('"'))) {
|
|
||||||
data = JSON.parse(data);
|
data = JSON.parse(data);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch { }
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return { data, dataBuffer };
|
return { data, dataBuffer };
|
||||||
};
|
};
|
||||||
|
@@ -398,14 +398,10 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
|
|||||||
// Filter out ZWNBSP character
|
// Filter out ZWNBSP character
|
||||||
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
|
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
|
||||||
data = data.replace(/^\uFEFF/, '');
|
data = data.replace(/^\uFEFF/, '');
|
||||||
|
if (!disableParsingResponseJson) {
|
||||||
// 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("\""))) {
|
|
||||||
data = JSON.parse(data);
|
data = JSON.parse(data);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch { }
|
||||||
console.log('Failed to parse response data as JSON');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { data, dataBuffer };
|
return { data, dataBuffer };
|
||||||
};
|
};
|
||||||
|
@@ -22,6 +22,12 @@ const toNumber = (value) => {
|
|||||||
return Number.isInteger(num) ? parseInt(value, 10) : parseFloat(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' }) => {
|
const executeQuickJsVm = ({ script: externalScript, context: externalContext, scriptType = 'template-literal' }) => {
|
||||||
if (!externalScript?.length || typeof externalScript !== 'string') {
|
if (!externalScript?.length || typeof externalScript !== 'string') {
|
||||||
@@ -29,8 +35,16 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
|
|||||||
}
|
}
|
||||||
externalScript = externalScript?.trim();
|
externalScript = externalScript?.trim();
|
||||||
|
|
||||||
|
if(scriptType === 'template-literal') {
|
||||||
if (!isNaN(Number(externalScript))) {
|
if (!isNaN(Number(externalScript))) {
|
||||||
return 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 === 'true') return true;
|
||||||
@@ -38,6 +52,8 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
|
|||||||
if (externalScript === 'null') return null;
|
if (externalScript === 'null') return null;
|
||||||
if (externalScript === 'undefined') return undefined;
|
if (externalScript === 'undefined') return undefined;
|
||||||
|
|
||||||
|
externalScript = removeQuotes(externalScript);
|
||||||
|
}
|
||||||
|
|
||||||
const vm = QuickJSSyncContext;
|
const vm = QuickJSSyncContext;
|
||||||
|
|
||||||
@@ -78,16 +94,6 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
|
|||||||
}
|
}
|
||||||
externalScript = externalScript?.trim();
|
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 {
|
try {
|
||||||
const module = await newQuickJSWASMModule();
|
const module = await newQuickJSWASMModule();
|
||||||
const vm = module.newContext();
|
const vm = module.newContext();
|
||||||
|
@@ -189,8 +189,8 @@ const addBruShimToContext = (vm, bru) => {
|
|||||||
const promise = vm.newPromise();
|
const promise = vm.newPromise();
|
||||||
bru.runRequest(vm.dump(args))
|
bru.runRequest(vm.dump(args))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const { status, headers, data, dataBuffer, size } = response || {};
|
const { status, statusText, headers, data, dataBuffer, size } = response || {};
|
||||||
promise.resolve(marshallToVm(cleanJson({ status, headers, data, dataBuffer, size }), vm));
|
promise.resolve(marshallToVm(cleanJson({ status, statusText, headers, data, dataBuffer, size }), vm));
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
promise.resolve(
|
promise.resolve(
|
||||||
|
@@ -85,6 +85,14 @@ const evaluateJsTemplateLiteral = (templateLiteral, context) => {
|
|||||||
return undefined;
|
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)) {
|
if (!isNaN(templateLiteral)) {
|
||||||
const number = Number(templateLiteral);
|
const number = Number(templateLiteral);
|
||||||
// Check if the number is too high. Too high number might get altered, see #1000
|
// Check if the number is too high. Too high number might get altered, see #1000
|
||||||
|
@@ -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}}"
|
||||||
|
}
|
22
packages/bruno-tests/collection/echo/test echo any.bru
Normal file
22
packages/bruno-tests/collection/echo/test echo any.bru
Normal file
@@ -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
|
||||||
|
}
|
22
packages/bruno-tests/collection/echo/test echo-any json.bru
Normal file
22
packages/bruno-tests/collection/echo/test echo-any json.bru
Normal file
@@ -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
|
||||||
|
}
|
@@ -1,7 +1,14 @@
|
|||||||
vars {
|
vars {
|
||||||
host: http://localhost:8080
|
host: http://localhost:8080
|
||||||
|
httpfaker: https://www.httpfaker.org
|
||||||
bearer_auth_token: your_secret_token
|
bearer_auth_token: your_secret_token
|
||||||
basic_auth_password: della
|
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_id: client_id_1
|
||||||
client_secret: client_secret_1
|
client_secret: client_secret_1
|
||||||
auth_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize
|
auth_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
vars {
|
vars {
|
||||||
host: https://testbench-sanity.usebruno.com
|
host: https://testbench-sanity.usebruno.com
|
||||||
|
httpfaker: https://www.httpfaker.org
|
||||||
bearer_auth_token: your_secret_token
|
bearer_auth_token: your_secret_token
|
||||||
basic_auth_password: della
|
basic_auth_password: della
|
||||||
env.var1: envVar1
|
env.var1: envVar1
|
||||||
|
@@ -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
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
@@ -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"'
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
@@ -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.
|
||||||
|
}
|
@@ -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": "<h1>hello</h1>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.body: eq <h1>hello</h1>
|
||||||
|
}
|
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
||||||
|
|
@@ -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"
|
||||||
|
}
|
@@ -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"
|
||||||
|
}
|
@@ -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"
|
||||||
|
}
|
@@ -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?"
|
||||||
|
}
|
@@ -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": "<message>hello</message>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.body: eq <message>hello</message>
|
||||||
|
}
|
@@ -48,4 +48,30 @@ router.get('/iso-enc', (req, res) => {
|
|||||||
return res.send(Buffer.from(responseText, 'latin1'));
|
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;
|
module.exports = router;
|
||||||
|
@@ -10,12 +10,19 @@ const multipartRouter = require('./multipart');
|
|||||||
const app = new express();
|
const app = new express();
|
||||||
const port = process.env.PORT || 8080;
|
const port = process.env.PORT || 8080;
|
||||||
|
|
||||||
app.use(express.raw({type: '*/*', limit: '100mb'}));
|
|
||||||
app.use(cors());
|
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(xmlParser());
|
||||||
app.use(bodyParser.text());
|
app.use(express.raw({ type: '*/*', limit: '100mb', verify: saveRawBody }));
|
||||||
app.use(bodyParser.json());
|
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
|
||||||
formDataParser.init(app, express);
|
formDataParser.init(app, express);
|
||||||
|
|
||||||
app.use('/api/auth', authRouter);
|
app.use('/api/auth', authRouter);
|
||||||
|
Reference in New Issue
Block a user