From 3742e69053ed2dc8f9c92352bd4e91f574b205ef Mon Sep 17 00:00:00 2001 From: Guillaume Date: Tue, 14 Jan 2025 13:34:56 +0100 Subject: [PATCH] Fix rules not able to target full targets (root level, no modifier) (#1646) Null and empty array rules were needing a modifier to work, which doesn't really make sense. Also, the default body string value was removed if the request contains a valid content type, even if parsing wasn't successful. Closes #1628 --- .../src/libs/response-rules-interpreter.ts | 59 +++---- .../response-rules-interpreter.test.ts | 154 ++++++++++++++++++ 2 files changed, 185 insertions(+), 28 deletions(-) diff --git a/packages/commons-server/src/libs/response-rules-interpreter.ts b/packages/commons-server/src/libs/response-rules-interpreter.ts index 06e0d739..c4e63d2b 100644 --- a/packages/commons-server/src/libs/response-rules-interpreter.ts +++ b/packages/commons-server/src/libs/response-rules-interpreter.ts @@ -139,26 +139,26 @@ export class ResponseRulesInterpreter { return false; } - let value: any; + let targetValue: any; const parsedRuleModifier = this.templateParse(rule.modifier ?? ''); // get the value for each rule type if (rule.target === 'request_number') { - value = requestNumber; + targetValue = requestNumber; } else if (rule.target === 'cookie') { if (!parsedRuleModifier) { return false; } - value = this.request.cookies?.[parsedRuleModifier]; + targetValue = this.request.cookies?.[parsedRuleModifier]; } else if (rule.target === 'path') { - value = this.targets.path; + targetValue = this.targets.path; } else if (rule.target === 'method') { - value = this.targets.method; + targetValue = this.targets.method; } else if (rule.target === 'header') { - value = this.request.header(parsedRuleModifier); + targetValue = this.request.header(parsedRuleModifier); } else if (rule.target === 'templating') { - value = parsedRuleModifier; + targetValue = parsedRuleModifier; } else { /** * Get the value for targets that can store complex data (body, query, params (route params), global_var, data_bucket) @@ -176,37 +176,39 @@ export class ResponseRulesInterpreter { : target; } - value = getValueFromPath(target, parsedRuleModifier, undefined); + targetValue = getValueFromPath(target, parsedRuleModifier, undefined); } else { /** * Body and query targets can be used without a modifier, in which case the whole parsed body or query is used. */ if (rule.target === 'body') { - value = requestMessage || this.targets.body; + targetValue = requestMessage || this.targets.body; } else if (rule.target === 'query') { - value = this.targets.query; + targetValue = this.targets.query; } } } - // ⬇ "null" and "empty_array" operators need no value - if (rule.operator === 'null' && parsedRuleModifier) { - return value === null || value === undefined; + // ⬇ "null" and "empty_array" operators need no rule value + if (rule.operator === 'null') { + return ( + targetValue === null || targetValue === undefined || targetValue === '' + ); } - if (rule.operator === 'empty_array' && parsedRuleModifier) { - return Array.isArray(value) && value.length < 1; + if (rule.operator === 'empty_array') { + return Array.isArray(targetValue) && targetValue.length < 1; } // ⬇ all other operators need a value - if (value === undefined) { + if (targetValue === undefined) { return false; } // value may be explicitely null (JSON), this is considered as an empty string - if (value === null) { - value = ''; + if (targetValue === null) { + targetValue = ''; } // rule value may be explicitely null (is shouldn't anymore), this is considered as an empty string too @@ -230,7 +232,7 @@ export class ResponseRulesInterpreter { const ajv = new Ajv(); addAjvFormats(ajv); - const valid = ajv.compile(schema)(value); + const valid = ajv.compile(schema)(targetValue); return valid; } catch (_error) { @@ -240,8 +242,8 @@ export class ResponseRulesInterpreter { if (rule.operator === 'array_includes' && rule.modifier) { return ( - Array.isArray(value) && - value.some((val) => String(val) === parsedRuleValue) + Array.isArray(targetValue) && + targetValue.some((val) => String(val) === parsedRuleValue) ); } @@ -253,17 +255,17 @@ export class ResponseRulesInterpreter { rule.operator === 'regex_i' ? 'i' : undefined ); - return Array.isArray(value) - ? value.some((arrayValue) => regex.test(arrayValue)) - : regex.test(value); + return Array.isArray(targetValue) + ? targetValue.some((arrayValue) => regex.test(arrayValue)) + : regex.test(targetValue); } // value extracted by JSONPath can be an array, cast its values to string (in line with the equals operator below) - if (Array.isArray(value)) { - return value.map((v) => String(v)).includes(parsedRuleValue); + if (Array.isArray(targetValue)) { + return targetValue.map((v) => String(v)).includes(parsedRuleValue); } - return String(value) === String(parsedRuleValue); + return String(targetValue) === String(parsedRuleValue); }; /** @@ -275,7 +277,8 @@ export class ResponseRulesInterpreter { if ( requestContentType && - stringIncludesArrayItems(ParsedBodyMimeTypes, requestContentType) + stringIncludesArrayItems(ParsedBodyMimeTypes, requestContentType) && + this.request.body !== undefined ) { body = this.request.body; } diff --git a/packages/commons-server/test/specs/response-rules/response-rules-interpreter.test.ts b/packages/commons-server/test/specs/response-rules/response-rules-interpreter.test.ts index c0a125e5..0b060220 100644 --- a/packages/commons-server/test/specs/response-rules/response-rules-interpreter.test.ts +++ b/packages/commons-server/test/specs/response-rules/response-rules-interpreter.test.ts @@ -2675,6 +2675,84 @@ describe('Response rules interpreter', () => { const xmlBody = 'John'; + it('should return response if full body is null (req content type absent)', () => { + const request: Request = { + header: function (headerName: string) { + const headers = {}; + + return headers[headerName]; + }, + stringBody: '', + body: '' + } as Request; + + const routeResponse = new ResponseRulesInterpreter( + [ + routeResponse403, + { + ...routeResponseTemplate, + rules: [ + { + target: 'body', + modifier: '', + value: '', + operator: 'null', + invert: false + } + ], + body: 'body1' + } + ], + fromExpressRequest(request), + null, + EnvironmentDefault, + [], + {}, + '' + ).chooseResponse(1); + strictEqual(routeResponse?.body, 'body1'); + }); + + it('should return response if full body is null (req content type present)', () => { + const request: Request = { + header: function (headerName: string) { + const headers = { + 'Content-Type': 'application/json' + }; + + return headers[headerName]; + }, + stringBody: '', + body: '' + } as Request; + + const routeResponse = new ResponseRulesInterpreter( + [ + routeResponse403, + { + ...routeResponseTemplate, + rules: [ + { + target: 'body', + modifier: '', + value: '', + operator: 'null', + invert: false + } + ], + body: 'body1' + } + ], + fromExpressRequest(request), + null, + EnvironmentDefault, + [], + {}, + '' + ).chooseResponse(1); + strictEqual(routeResponse?.body, 'body1'); + }); + it('should return response if full body value matches (no modifier + regex)', () => { const request: Request = { header: function (headerName: string) { @@ -4201,6 +4279,82 @@ describe('Response rules interpreter', () => { ).chooseResponse(1); strictEqual(routeResponse?.body, 'unauthorized'); }); + + it('should return response if operator is "empty_array" and body property is an empty array', () => { + const request: Request = { + header: function (headerName: string) { + const headers = { 'Content-Type': 'application/json' }; + + return headers[headerName]; + }, + body: { + prop: [] + } + } as Request; + + const routeResponse = new ResponseRulesInterpreter( + [ + routeResponse403, + { + ...routeResponseTemplate, + rules: [ + { + target: 'body', + modifier: 'prop', + value: '', + operator: 'empty_array', + invert: false + } + ], + body: 'response1' + } + ], + fromExpressRequest(request), + null, + EnvironmentDefault, + [], + {}, + '' + ).chooseResponse(1); + strictEqual(routeResponse?.body, 'response1'); + }); + + it('should return response if operator is "empty_array" and body is an empty array', () => { + const request: Request = { + header: function (headerName: string) { + const headers = { 'Content-Type': 'application/json' }; + + return headers[headerName]; + }, + body: [] + } as Request; + + const routeResponse = new ResponseRulesInterpreter( + [ + routeResponse403, + { + ...routeResponseTemplate, + rules: [ + { + target: 'body', + modifier: '', + value: '', + operator: 'empty_array', + invert: false + } + ], + body: 'response1' + } + ], + fromExpressRequest(request), + null, + EnvironmentDefault, + [], + {}, + '' + ).chooseResponse(1); + strictEqual(routeResponse?.body, 'response1'); + }); }); describe('Complex rules (AND/OR)', () => {