From 5d4c73054445f99d37c16044c5be841b3b384c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 12 Sep 2016 16:46:13 +0200 Subject: [PATCH 1/2] Reject array/date values for object arguments Having to distinguish between an array and a plain object in remote methods is tedious. For example, LoopBack Model constructors expect an object as the first argument, but will accept an array too, with undefined result. This commit changes the implementation of object converter to reject values that are of a different strong-remoting type, namely "array" and "date" --- 3.0-RELEASE-NOTES.md | 1 + lib/looks-like-json.js | 27 +++++++++++++++++ lib/types/any.js | 17 +++++++---- lib/types/array.js | 6 ++-- lib/types/object.js | 30 +++++++++++++------ test/rest-coercion/jsonbody-object.suite.js | 13 +++++--- test/rest-coercion/jsonform-object.suite.js | 17 ++++++----- test/rest-coercion/urlencoded-object.suite.js | 17 ++++++----- test/rest.test.js | 15 ---------- 9 files changed, 90 insertions(+), 53 deletions(-) create mode 100644 lib/looks-like-json.js diff --git a/3.0-RELEASE-NOTES.md b/3.0-RELEASE-NOTES.md index ce00ca80..0954b4ae 100644 --- a/3.0-RELEASE-NOTES.md +++ b/3.0-RELEASE-NOTES.md @@ -173,6 +173,7 @@ Most notable breaking changes: the value as a number now. That way the value "0" always produces "1970-01-01T00:00:00.000Z" instead of some date around 1999/2000/2001 depending on server timezone. + - Array values are not allowed for arguments of type object. Hopefully this change should leave most LoopBack applications (and clients) unaffected. If your start seeing unusual amount of 400 error responses after diff --git a/lib/looks-like-json.js b/lib/looks-like-json.js new file mode 100644 index 00000000..2b45cdb7 --- /dev/null +++ b/lib/looks-like-json.js @@ -0,0 +1,27 @@ +// Copyright IBM Corp. 2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + +'use strict'; + +module.exports = { + looksLikeJson: looksLikeJson, + looksLikeJsonArray: looksLikeJsonArray, + looksLikeJsonObject: looksLikeJsonObject, +}; + +function looksLikeJson(value) { + return looksLikeJsonObject(value) || looksLikeJsonArray(value); +} + +function looksLikeJsonArray(value) { + return typeof value === 'string' && + value[0] === '[' && value[value.length - 1] === ']'; +} + +function looksLikeJsonObject(value) { + return typeof value === 'string' && + value[0] === '{' && value[value.length - 1] === '}'; +} + diff --git a/lib/types/any.js b/lib/types/any.js index 1706f3b1..3c406e00 100644 --- a/lib/types/any.js +++ b/lib/types/any.js @@ -8,6 +8,7 @@ var debug = require('debug')('strong-remoting:http-coercion'); var g = require('strong-globalize')(); var numberChecks = require('../number-checks'); +var looksLikeJson = require('../looks-like-json').looksLikeJson; var MAX_SAFE_INTEGER = numberChecks.MAX_SAFE_INTEGER; var MIN_SAFE_INTEGER = numberChecks.MIN_SAFE_INTEGER; @@ -46,12 +47,18 @@ module.exports = { value = num; } - var objectConverter = ctx.typeRegistry.getConverter('object'); - var objectResult = objectConverter.fromSloppyValue(ctx, value); - if (!objectResult.error && objectResult.value) - return objectResult; + if (looksLikeJson(value)) { + try { + var result = JSON.parse(value); + debug('parsed %j as JSON: %j', value, result); + return this.fromTypedValue(ctx, result); + } catch (ex) { + debug('Cannot parse "any" value %j, assuming string. %s', value, ex); + // no-op, use the original string value + } + } - return { value: value }; + return this.fromTypedValue(ctx, value); }, validate: function(ctx, value) { diff --git a/lib/types/array.js b/lib/types/array.js index 831a1ae0..06d50faf 100644 --- a/lib/types/array.js +++ b/lib/types/array.js @@ -9,6 +9,7 @@ var assert = require('assert'); var debug = require('debug')('strong-remoting:http-coercion'); var escapeRegex = require('escape-string-regexp'); var g = require('strong-globalize')(); +var looksLikeJsonArray = require('../looks-like-json').looksLikeJsonArray; module.exports = ArrayConverter; @@ -56,10 +57,7 @@ ArrayConverter.prototype.fromSloppyValue = function(ctx, value) { }; ArrayConverter.prototype._fromTypedValueString = function(ctx, value) { - var looksLikeJsonArray = typeof value === 'string' && - value[0] === '[' && value[value.length - 1] === ']'; - - if (!looksLikeJsonArray) + if (!looksLikeJsonArray(value)) return null; // If it looks like a JSON array, try to parse it. diff --git a/lib/types/object.js b/lib/types/object.js index 69373a7e..ad493bf1 100644 --- a/lib/types/object.js +++ b/lib/types/object.js @@ -7,6 +7,7 @@ var debug = require('debug')('strong-remoting:http-coercion'); var g = require('strong-globalize')(); +var looksLikeJsonObject = require('../looks-like-json').looksLikeJsonObject; module.exports = { fromTypedValue: function(ctx, value) { @@ -23,11 +24,7 @@ module.exports = { if (value === null || value === 'null') return { value: null }; - var looksLikeJson = typeof value === 'string' && ( - (value[0] === '[' && value[value.length - 1] === ']') || - (value[0] === '{' && value[value.length - 1] === '}')); - - if (looksLikeJson) { + if (looksLikeJsonObject(value)) { try { var result = JSON.parse(value); debug('parsed %j as JSON: %j', value, result); @@ -45,11 +42,26 @@ module.exports = { }, validate: function(ctx, value) { - if (value === undefined || typeof value === 'object') + if (value === undefined || value === null) return null; - var err = new Error(g.f('Value is not an object.')); - err.statusCode = 400; - return err; + if (typeof value !== 'object') + return errorNotAnObject(); + + // reject object-like values that have their own strong-remoting type + + if (Array.isArray(value)) + return errorNotAnObject(); + + if (value instanceof Date) + return errorNotAnObject(); + + return null; }, }; + +function errorNotAnObject() { + var err = new Error(g.f('Value is not an object.')); + err.statusCode = 400; + return err; +} diff --git a/test/rest-coercion/jsonbody-object.suite.js b/test/rest-coercion/jsonbody-object.suite.js index 99eae27b..298243b3 100644 --- a/test/rest-coercion/jsonbody-object.suite.js +++ b/test/rest-coercion/jsonbody-object.suite.js @@ -16,14 +16,16 @@ module.exports = function(ctx) { // See verifyTestCases' jsdoc for details about the format of test cases. verifyTestCases({ arg: 'anyname', type: 'object', required: true }, [ // Valid values, arrays are objects too - [[]], // an empty array is a valid value [{}], // an empty object is a valid value too [{ x: '' }], [{ x: null }], - [[1, 2]], // Invalid values trigger ERROR_BAD_REQUEST [null, ERROR_BAD_REQUEST], + + // Arrays are not allowed + [[], ERROR_BAD_REQUEST], + [[1, 2], ERROR_BAD_REQUEST], ]); }); @@ -33,8 +35,7 @@ module.exports = function(ctx) { // Empty values [null, null], - // Valid values, arrays are objects too - [[]], + // Valid values [{}], // Verify that deep coercion is not triggered @@ -68,6 +69,10 @@ module.exports = function(ctx) { [{ x: '2016-05-19T13:28:51.299Z' }], [{ x: '2016-05-19' }], [{ x: 'Thu May 19 2016 15:28:51 GMT 0200 (CEST)' }], + + // Arrays are not allowed + [[], ERROR_BAD_REQUEST], + [[1, 2], ERROR_BAD_REQUEST], ]); }); }; diff --git a/test/rest-coercion/jsonform-object.suite.js b/test/rest-coercion/jsonform-object.suite.js index fd798ed4..b12c5920 100644 --- a/test/rest-coercion/jsonform-object.suite.js +++ b/test/rest-coercion/jsonform-object.suite.js @@ -19,14 +19,15 @@ module.exports = function(ctx) { // Valid values [{ arg: {}}, {}], [{ arg: { foo: 'bar' }}, { foo: 'bar' }], - // Arrays are objects too - [{ arg: [] }, []], - [{ arg: [1, 2] }, [1, 2]], // Empty values should trigger ERROR_BAD_REQUEST [EMPTY_BODY, ERROR_BAD_REQUEST], [{ arg: null }, ERROR_BAD_REQUEST], [{ arg: '' }, ERROR_BAD_REQUEST], + + // Arrays are not allowed + [{ arg: [] }, ERROR_BAD_REQUEST], + [{ arg: [1, 2] }, ERROR_BAD_REQUEST], ]); }); @@ -43,11 +44,6 @@ module.exports = function(ctx) { [{ arg: { x: 'value' }}, { x: 'value' }], [{ arg: { x: 1 }}, { x: 1 }], - // Arrays are objects too - [{ arg: [] }, []], - [{ arg: ['text'] }, ['text']], - [{ arg: [1, 2] }, [1, 2]], - // Verify that deep coercion is not triggered // and types specified in JSON are preserved [{ arg: { x: '1' }}, { x: '1' }], @@ -67,6 +63,11 @@ module.exports = function(ctx) { [{ arg: 0 }, ERROR_BAD_REQUEST], [{ arg: 1 }, ERROR_BAD_REQUEST], [{ arg: -1 }, ERROR_BAD_REQUEST], + + // Arrays are not allowed + [{ arg: [] }, ERROR_BAD_REQUEST], + [{ arg: ['text'] }, ERROR_BAD_REQUEST], + [{ arg: [1, 2] }, ERROR_BAD_REQUEST], ]); }); }; diff --git a/test/rest-coercion/urlencoded-object.suite.js b/test/rest-coercion/urlencoded-object.suite.js index 02abcc9c..893e2713 100644 --- a/test/rest-coercion/urlencoded-object.suite.js +++ b/test/rest-coercion/urlencoded-object.suite.js @@ -23,9 +23,6 @@ function suite(prefix, ctx) { // Valid values - JSON encoding ['arg={}', {}], ['arg={"foo":"bar"}', { foo: 'bar' }], - // arrays are objects too - ['arg=[]', []], - ['arg=[1,2]', [1, 2]], // Valid values - nested keys ['arg[key]=undefined', { key: 'undefined' }], @@ -39,6 +36,10 @@ function suite(prefix, ctx) { // Empty-like values should trigger ERROR_BAD_REQUEST too ['arg=null', ERROR_BAD_REQUEST], ['arg=undefined', ERROR_BAD_REQUEST], + + // Arrays are not allowed + ['arg=[]', ERROR_BAD_REQUEST], + ['arg=[1,2]', ERROR_BAD_REQUEST], ]); }); @@ -51,7 +52,6 @@ function suite(prefix, ctx) { ['arg=', undefined], ['arg=null', null], ['arg={}', {}], - ['arg=[]', []], // Valid values - nested keys // Nested values are NOT coerced (no deep coercion) @@ -92,7 +92,6 @@ function suite(prefix, ctx) { ['arg={"key":-1}', { key: -1 }], ['arg={"key":1.2}', { key: 1.2 }], ['arg={"key":-1.2}', { key: -1.2 }], - ['arg=["text"]', ['text']], // Nested values are NOT coerced (no deep coercion) ['arg={"key":"false"}', { key: 'false' }], ['arg={"key":"true"}', { key: 'true' }], @@ -102,9 +101,11 @@ function suite(prefix, ctx) { ['arg={"key":"1.2"}', { key: '1.2' }], ['arg={"key":"-1.2"}', { key: '-1.2' }], - // arrays are objects too - ['arg=[1,2]', [1, 2]], - ['arg=[1,"text"]', [1, 'text']], + // Arrays are not allowed + ['arg=[]', ERROR_BAD_REQUEST], + ['arg=[1,2]', ERROR_BAD_REQUEST], + ['arg=[1,"text"]', ERROR_BAD_REQUEST], + ['arg=["text"]', ERROR_BAD_REQUEST], // Invalid values should trigger ERROR_BAD_REQUEST ['arg=undefined', ERROR_BAD_REQUEST], diff --git a/test/rest.test.js b/test/rest.test.js index 631a487b..178c1482 100644 --- a/test/rest.test.js +++ b/test/rest.test.js @@ -1073,21 +1073,6 @@ describe('strong-remoting-rest', function() { .end(done); }); - it('should not flatten arrays for target type "object"', function(done) { - var method = givenSharedStaticMethod( - function(arg, cb) { cb(null, { value: arg }); }, - { - accepts: { arg: 'arg', type: 'object' }, - returns: { arg: 'data', type: 'any', root: true }, - http: { method: 'POST' }, - }); - - request(app).post(method.url) - .send({ arg: ['single'] }) - .expect(200, { value: ['single'] }) - .end(done); - }); - it('should support taget type [any]', function(done) { var method = givenSharedStaticMethod( function(arg, cb) { cb(null, { value: arg }); }, From 4b0b2e8f371bc6e136fb96130d48abe36605b7ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 12 Sep 2016 16:49:52 +0200 Subject: [PATCH 2/2] Add rest-coercion tests for custom object types --- test/rest-coercion.test.js | 16 ++- test/rest-coercion/_custom-class.context.js | 51 ++++++++ test/rest-coercion/_jsonbody.context.js | 4 +- test/rest-coercion/_jsonform.context.js | 4 +- test/rest-coercion/_urlencoded.context.js | 4 +- .../jsonbody-object-type.suite.js | 72 +++++++++++ .../jsonform-object-type.suite.js | 79 ++++++++++++ .../urlencoded-object-type.suite.js | 116 ++++++++++++++++++ 8 files changed, 339 insertions(+), 7 deletions(-) create mode 100644 test/rest-coercion/_custom-class.context.js create mode 100644 test/rest-coercion/jsonbody-object-type.suite.js create mode 100644 test/rest-coercion/jsonform-object-type.suite.js create mode 100644 test/rest-coercion/urlencoded-object-type.suite.js diff --git a/test/rest-coercion.test.js b/test/rest-coercion.test.js index 7fadd8db..3edf6312 100644 --- a/test/rest-coercion.test.js +++ b/test/rest-coercion.test.js @@ -16,7 +16,7 @@ var RemoteObjects = require('..'); describe('Coercion in RestAdapter', function() { var ctx = { - remoteObject: null, + remoteObjects: null, request: null, ERROR_BAD_REQUEST: new Error(400), prettyExpectation: prettyExpectation, @@ -107,10 +107,24 @@ describe('Coercion in RestAdapter', function() { { value: actualValue } : { error: res.statusCode }; + var actualCtor = actual.value && typeof actual.value === 'object' && + actual.value.constructor; + if (actualCtor && actualCtor !== Object && actualCtor.name) { + actual = {}; + actual[actualCtor.name] = actualValue; + } + var expected = expectedResult instanceof Error ? { error: +expectedResult.message } : { value: expectedResult }; + var expectedCtor = expected.value && typeof expected.value === 'object' && + expected.value.constructor; + if (expectedCtor && expectedCtor !== Object && expectedCtor.name) { + expected = {}; + expected[expectedCtor.name] = expectedResult; + } + var suiteName = ctx.runtime.currentSuiteName; var input = ctx.runtime.currentInput; if (suiteName && input) { diff --git a/test/rest-coercion/_custom-class.context.js b/test/rest-coercion/_custom-class.context.js new file mode 100644 index 00000000..7995b2fa --- /dev/null +++ b/test/rest-coercion/_custom-class.context.js @@ -0,0 +1,51 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + +'use strict'; + +var extend = require('util')._extend; + +function CustomClass(data) { + if (!(this instanceof CustomClass)) + return new CustomClass(data); + + if (data.invalid) { + var err = new Error('Invalid CustomClass value.'); + err.statusCode = 400; + throw err; + } + + if ('name' in data) + this.name = data.name; + else + this.empty = true; +}; + +module.exports = function createCustomClassContext(ctx) { + beforeEach(function registerCustomClass() { + if ('customclass' in ctx.remoteObjects._typeRegistry._types) { + // This happens when there are multiple instances of this beforEach hook + // registered. Typically when createCustomClassContext is called + // inside the top-level "describe" block. + return; + } + ctx.remoteObjects.defineObjectType('CustomClass', CustomClass); + }); + + return extend(Object.create(ctx), { + CustomClass: CustomClass, + verifyTestCases: verifyTestCases, + }); + + function verifyTestCases(argSpec, testCases) { + for (var ix in testCases) { + if (testCases[ix].length === 1) { + var data = testCases[ix][0]; + testCases[ix] = [data, new CustomClass(data)]; + } + } + ctx.verifyTestCases(argSpec, testCases); + } +}; diff --git a/test/rest-coercion/_jsonbody.context.js b/test/rest-coercion/_jsonbody.context.js index 06132736..a863e48a 100644 --- a/test/rest-coercion/_jsonbody.context.js +++ b/test/rest-coercion/_jsonbody.context.js @@ -12,9 +12,9 @@ var format = util.format; var extend = util._extend; module.exports = function createJsonBodyContext(ctx) { - return extend({ + return extend(Object.create(ctx), { verifyTestCases: verifyTestCases, - }, ctx); + }); /** * Verify a set of test-cases for a given argument specification diff --git a/test/rest-coercion/_jsonform.context.js b/test/rest-coercion/_jsonform.context.js index 62c3df08..83759468 100644 --- a/test/rest-coercion/_jsonform.context.js +++ b/test/rest-coercion/_jsonform.context.js @@ -14,11 +14,11 @@ var extend = util._extend; var EMPTY_BODY = {}; module.exports = function createJsonBodyContext(ctx) { - return extend({ + return extend(Object.create(ctx), { /** Send a request with an empty body (that is still valid JSON) */ EMPTY_BODY: EMPTY_BODY, verifyTestCases: verifyTestCases, - }, ctx); + }); /** * Verify a set of test-cases for a given argument specification diff --git a/test/rest-coercion/_urlencoded.context.js b/test/rest-coercion/_urlencoded.context.js index b0c94591..aae91594 100644 --- a/test/rest-coercion/_urlencoded.context.js +++ b/test/rest-coercion/_urlencoded.context.js @@ -16,11 +16,11 @@ var EMPTY_QUERY = ''; module.exports = function createUrlEncodedContext(ctx, target) { var TARGET_QUERY_STRING = target === 'qs'; - return extend({ + return extend(Object.create(ctx), { /** Send empty data, i.e. empty request body or no query string */ EMPTY_QUERY: EMPTY_QUERY, verifyTestCases: verifyTestCases, - }, ctx); + }); /** * Verify a set of test-cases for a given argument specification diff --git a/test/rest-coercion/jsonbody-object-type.suite.js b/test/rest-coercion/jsonbody-object-type.suite.js new file mode 100644 index 00000000..f99cac5b --- /dev/null +++ b/test/rest-coercion/jsonbody-object-type.suite.js @@ -0,0 +1,72 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + +'use strict'; + +var jsonBodyContext = require('./_jsonbody.context'); +var customClassContext = require('./_custom-class.context.js'); + +module.exports = function(ctx) { + ctx = customClassContext(jsonBodyContext(ctx)); + var ERROR_BAD_REQUEST = ctx.ERROR_BAD_REQUEST; + var CustomClass = ctx.CustomClass; + var verifyTestCases = ctx.verifyTestCases; + + describe('json body - CustomClass - required', function() { + // See verifyTestCases' jsdoc for details about the format of test cases. + verifyTestCases({ arg: 'anyname', type: 'CustomClass', required: true }, [ + // An empty object is a valid value + [{}], + + [{ name: '' }], + [{ name: 'a-test-name' }], + + // Invalid values trigger ERROR_BAD_REQUEST + [null, ERROR_BAD_REQUEST], + [{ invalid: true }, ERROR_BAD_REQUEST], + + // Array values are not allowed + [[], ERROR_BAD_REQUEST], + [[1, 2], ERROR_BAD_REQUEST], + ]); + }); + + describe('json body - CustomClass - optional', function() { + // See verifyTestCases' jsdoc for details about the format of test cases. + verifyTestCases({ arg: 'anyname', type: 'CustomClass' }, [ + // Empty values + [null, null], + + // Valid values + [{}], + [{ name: 'a-test-name' }], + + // Verify that deep coercion is not triggered + // and types specified in JSON are preserved + + [{ name: '' }], + [{ name: null }], + [{ name: {}}], + [{ name: { key: null }}], + [{ name: 1 }], + [{ name: '1' }], + [{ name: -1 }], + [{ name: '-1' }], + [{ name: 1.2 }], + [{ name: '1.2' }], + [{ name: -1.2 }], + [{ name: '-1.2' }], + [{ name: ['tenamet'] }], + [{ name: [1, 2] }], + + // Invalid values - arrays are rejected + [[], ERROR_BAD_REQUEST], + [[1, 2], ERROR_BAD_REQUEST], + + // Verify that errors thrown by the factory function are handled + [{ invalid: true }, ERROR_BAD_REQUEST], + ]); + }); +}; diff --git a/test/rest-coercion/jsonform-object-type.suite.js b/test/rest-coercion/jsonform-object-type.suite.js new file mode 100644 index 00000000..00992ea1 --- /dev/null +++ b/test/rest-coercion/jsonform-object-type.suite.js @@ -0,0 +1,79 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + +'use strict'; + +var jsonFormContext = require('./_jsonform.context'); +var customClassContext = require('./_custom-class.context.js'); + +module.exports = function(ctx) { + ctx = customClassContext(jsonFormContext(ctx)); + var EMPTY_BODY = ctx.EMPTY_BODY; + var ERROR_BAD_REQUEST = ctx.ERROR_BAD_REQUEST; + var CustomClass = ctx.CustomClass; + var verifyTestCases = ctx.verifyTestCases; + + describe('json form - CustomClass - required', function() { + // See verifyTestCases' jsdoc for details about the format of test cases. + verifyTestCases({ arg: 'arg', type: 'CustomClass', required: true }, [ + // Valid values + [{ arg: {}}, CustomClass({})], + [{ arg: { foo: 'bar' }}, CustomClass({ foo: 'bar' })], + + // Empty values should trigger ERROR_BAD_REQUEST + [EMPTY_BODY, ERROR_BAD_REQUEST], + [{ arg: null }, ERROR_BAD_REQUEST], + [{ arg: '' }, ERROR_BAD_REQUEST], + + // Arrays are not allowed + [{ arg: [] }, ERROR_BAD_REQUEST], + [{ arg: [1, 2] }, ERROR_BAD_REQUEST], + ]); + }); + + describe('json form - CustomClass - optional', function() { + // See verifyTestCases' jsdoc for details about the format of test cases. + verifyTestCases({ arg: 'arg', type: 'CustomClass' }, [ + // Empty values + [EMPTY_BODY, undefined], + [{ arg: null }, null], + + // Valid values + [{ arg: { name: null }}, CustomClass({ name: null })], + [{ arg: {}}, CustomClass({})], + [{ arg: { name: 'value' }}, CustomClass({ name: 'value' })], + [{ arg: { name: 1 }}, CustomClass({ name: 1 })], + + + // Verify that deep coercion is not triggered + // and types specified in JSON are preserved + [{ arg: { name: '1' }}, CustomClass({ name: '1' })], + [{ arg: { name: -1 }}, CustomClass({ name: -1 })], + [{ arg: { name: '-1' }}, CustomClass({ name: '-1' })], + [{ arg: { name: 1.2 }}, CustomClass({ name: 1.2 })], + [{ arg: { name: '1.2' }}, CustomClass({ name: '1.2' })], + [{ arg: { name: -1.2 }}, CustomClass({ name: -1.2 })], + [{ arg: { name: '-1.2' }}, CustomClass({ name: '-1.2' })], + [{ arg: { name: 'true' }}, CustomClass({ name: 'true' })], + [{ arg: { name: 'false' }}, CustomClass({ name: 'false' })], + + // Invalid values should trigger ERROR_BAD_REQUEST + [{ arg: '' }, ERROR_BAD_REQUEST], + [{ arg: false }, ERROR_BAD_REQUEST], + [{ arg: true }, ERROR_BAD_REQUEST], + [{ arg: 0 }, ERROR_BAD_REQUEST], + [{ arg: 1 }, ERROR_BAD_REQUEST], + [{ arg: -1 }, ERROR_BAD_REQUEST], + + // Arrays are not allowed + [{ arg: [] }, ERROR_BAD_REQUEST], + [{ arg: ['text'] }, ERROR_BAD_REQUEST], + [{ arg: [1, 2] }, ERROR_BAD_REQUEST], + + // Verify that errors thrown by the factory function are handled + [{ arg: { invalid: true }}, ERROR_BAD_REQUEST], + ]); + }); +}; diff --git a/test/rest-coercion/urlencoded-object-type.suite.js b/test/rest-coercion/urlencoded-object-type.suite.js new file mode 100644 index 00000000..8387a4e8 --- /dev/null +++ b/test/rest-coercion/urlencoded-object-type.suite.js @@ -0,0 +1,116 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + +'use strict'; + +var urlEncodedContext = require('./_urlencoded.context'); +var customClassContext = require('./_custom-class.context.js'); + +module.exports = function(ctx) { + suite('query string', urlEncodedContext(ctx, 'qs')); + suite('form data', urlEncodedContext(ctx, 'form')); +}; + +function suite(prefix, ctx) { + ctx = customClassContext(ctx); + var EMPTY_QUERY = ctx.EMPTY_QUERY; + var ERROR_BAD_REQUEST = ctx.ERROR_BAD_REQUEST; + var CustomClass = ctx.CustomClass; + var verifyTestCases = ctx.verifyTestCases; + + describe(prefix + ' - CustomClass - required', function() { + // See verifyTestCases' jsdoc for details about the format of test cases. + verifyTestCases({ arg: 'arg', type: 'CustomClass', required: true }, [ + // Valid values - JSON encoding + ['arg={}', CustomClass({})], + ['arg={"name":"bar"}', CustomClass({ name: 'bar' })], + + // Valid values - nested keys + ['arg[name]=undefined', CustomClass({ name: 'undefined' })], + ['arg[name]=null', CustomClass({ name: 'null' })], + ['arg[name]=text', CustomClass({ name: 'text' })], + + // Empty values should trigger ERROR_BAD_REQUEST + [EMPTY_QUERY, ERROR_BAD_REQUEST], + ['arg', ERROR_BAD_REQUEST], + ['arg=', ERROR_BAD_REQUEST], + // Empty-like values should trigger ERROR_BAD_REQUEST too + ['arg=null', ERROR_BAD_REQUEST], + ['arg=undefined', ERROR_BAD_REQUEST], + + // Arrays are not allowed + ['arg=[]', ERROR_BAD_REQUEST], + ['arg=[1,2]', ERROR_BAD_REQUEST], + ]); + }); + + describe(prefix + ' - CustomClass - optional', function() { + // See verifyTestCases' jsdoc for details about the format of test cases. + verifyTestCases({ arg: 'arg', type: 'CustomClass' }, [ + // Empty values + [EMPTY_QUERY, undefined], + ['arg', undefined], + ['arg=', undefined], + ['arg=null', null], + ['arg={}', CustomClass({})], + + // Valid values - nested keys + // Nested values are NOT coerced (no deep coercion) + ['arg[name]=undefined', CustomClass({ name: 'undefined' })], + ['arg[name]=null', CustomClass({ name: 'null' })], + ['arg[name]=value', CustomClass({ name: 'value' })], + ['arg[name]=0', CustomClass({ name: '0' })], + ['arg[name]=1', CustomClass({ name: '1' })], + ['arg[name]=-1', CustomClass({ name: '-1' })], + ['arg[name]=1.2', CustomClass({ name: '1.2' })], + ['arg[name]=-1.2', CustomClass({ name: '-1.2' })], + ['arg[name]=true', CustomClass({ name: 'true' })], + ['arg[name]=false', CustomClass({ name: 'false' })], + ['arg[x]=a&arg[y]=b', CustomClass({ x: 'a', y: 'b' })], + ['arg[name]=[1,2]', CustomClass({ name: '[1,2]' })], + ['arg[name]=1&arg[name]=2', CustomClass({ name: ['1', '2'] })], + + // Valid values - JSON encoding + ['arg={"name":null}', CustomClass({ name: null })], + ['arg={"name":"value"}', CustomClass({ name: 'value' })], + ['arg={"name":false}', CustomClass({ name: false })], + ['arg={"name":true}', CustomClass({ name: true })], + ['arg={"name":0}', CustomClass({ name: 0 })], + ['arg={"name":1}', CustomClass({ name: 1 })], + ['arg={"name":-1}', CustomClass({ name: -1 })], + ['arg={"name":1.2}', CustomClass({ name: 1.2 })], + ['arg={"name":-1.2}', CustomClass({ name: -1.2 })], + // Nested values are NOT coerced (no deep coercion) + ['arg={"name":"false"}', CustomClass({ name: 'false' })], + ['arg={"name":"true"}', CustomClass({ name: 'true' })], + ['arg={"name":"0"}', CustomClass({ name: '0' })], + ['arg={"name":"1"}', CustomClass({ name: '1' })], + ['arg={"name":"-1"}', CustomClass({ name: '-1' })], + ['arg={"name":"1.2"}', CustomClass({ name: '1.2' })], + ['arg={"name":"-1.2"}', CustomClass({ name: '-1.2' })], + + + // Invalid values should trigger ERROR_BAD_REQUEST + ['arg=undefined', ERROR_BAD_REQUEST], + ['arg=false', ERROR_BAD_REQUEST], + ['arg=true', ERROR_BAD_REQUEST], + ['arg=0', ERROR_BAD_REQUEST], + ['arg=1', ERROR_BAD_REQUEST], + ['arg=-1', ERROR_BAD_REQUEST], + ['arg=text', ERROR_BAD_REQUEST], + ['arg={malformed}', ERROR_BAD_REQUEST], + ['arg=[malformed]', ERROR_BAD_REQUEST], + + // arrays are not allowed + ['arg=[]', ERROR_BAD_REQUEST], + ['arg=["text"]', ERROR_BAD_REQUEST], + ['arg=[1,2]', ERROR_BAD_REQUEST], + ['arg=[1,"text"]', ERROR_BAD_REQUEST], + + // Verify that errors thrown by the factory function are handled + ['arg[invalid]=true', ERROR_BAD_REQUEST], + ]); + }); +}