diff --git a/src/core/assertions.js b/src/core/assertions.js index 8de65063..ef02bb16 100644 --- a/src/core/assertions.js +++ b/src/core/assertions.js @@ -28,41 +28,48 @@ const RuleName = Object.freeze({ }) /** - * Count object properties including nested objects ones. - * If a property is an object, its key is ignored. + * Make an array of keys from an object. + * Use a dot notation + * Only empty object keys are taken into account + * For subobjects only keys are taken into account * * @example - * Assertions.countNestedProperties({ + * Assertions.buildObjectKeysArray({ * a: true, * b: true, * c: true, * }) - * // => 3 - * Assertions.countNestedProperties({ + * // => ["a", "b", "c"] + * Assertions.buildObjectKeysArray({ * a: true, * b: true, * c: { - * a: true, - * b: true, + * d: true, + * e: {}, + * f: { + * g: true + * } * }, * }) - * // => 4 (c is ignored because it's a nested object) + * // => ["a", "b", "c.d", "c.e", "c.f.g"] (c and c.f are ignored as non empty nested objects) * * @param {Object} object * @return {number} */ -exports.countNestedProperties = (object) => { - let propertiesCount = 0 +exports.buildObjectKeysArray = (object, dottedObjectKeys = [], currentPath = '') => { Object.keys(object).forEach((key) => { if (!_.isEmpty(object[key]) && typeof object[key] === 'object') { - const count = exports.countNestedProperties(object[key]) - propertiesCount += count + dottedObjectKeys = exports.buildObjectKeysArray( + object[key], + dottedObjectKeys, + `${currentPath}${key}.` + ) } else { - propertiesCount++ + dottedObjectKeys.push(`${currentPath}${key}`) } }) - return propertiesCount + return dottedObjectKeys } /** @@ -106,10 +113,11 @@ exports.countNestedProperties = (object) => { * @param {boolean} [exact=false] - if `true`, specification must match all object's properties */ exports.assertObjectMatchSpec = (object, spec, exact = false) => { + const specPath = new Set() spec.forEach(({ field, matcher, value }) => { const currentValue = _.get(object, field) const expectedValue = Cast.value(value) - + specPath.add(field) const rule = exports.getMatchingRule(matcher) switch (rule.name) { @@ -208,11 +216,12 @@ exports.assertObjectMatchSpec = (object, spec, exact = false) => { // We check we have exactly the same number of properties as expected if (exact === true) { - const propertiesCount = exports.countNestedProperties(object) + const objectKeys = exports.buildObjectKeysArray(object) + const specObjectKeys = Array.from(specPath) expect( - propertiesCount, + objectKeys, 'Expected json response to fully match spec, but it does not' - ).to.be.equal(spec.length) + ).to.be.deep.equal(specObjectKeys) } } diff --git a/tests/core/assertions.test.js b/tests/core/assertions.test.js index b2f6e3b3..fd14d567 100644 --- a/tests/core/assertions.test.js +++ b/tests/core/assertions.test.js @@ -1,6 +1,10 @@ 'use strict' -const { countNestedProperties, assertObjectMatchSpec } = require('../../src/core/assertions') +const { + countNestedProperties, + assertObjectMatchSpec, + buildObjectKeysArray, +} = require('../../src/core/assertions') beforeAll(() => { const MockDate = (lastDate) => () => new lastDate(2018, 4, 1) @@ -13,17 +17,17 @@ afterAll(() => { beforeEach(() => {}) -test('should allow to count object properties', () => { +test('should allow to build an array of object properties', () => { expect( - countNestedProperties({ + buildObjectKeysArray({ a: true, b: true, c: true, }) - ).toBe(3) + ).toStrictEqual(['a', 'b', 'c']) expect( - countNestedProperties({ + buildObjectKeysArray({ a: true, b: true, c: true, @@ -32,12 +36,12 @@ test('should allow to count object properties', () => { b: true, }, }) - ).toBe(5) + ).toStrictEqual(['a', 'b', 'c', 'd.a', 'd.b']) }) -test('should allow to count nested objects properties', () => { +test('should allow to build an array of object propertieswith nested objects properties', () => { expect( - countNestedProperties({ + buildObjectKeysArray({ a: true, b: true, c: { @@ -45,32 +49,32 @@ test('should allow to count nested objects properties', () => { e: 'value2', }, }) - ).toBe(4) + ).toStrictEqual(['a', 'b', 'c.d', 'c.e']) }) -test('should allow to count object properties with null, undefined properties ', () => { +test('should allow to build an array of object properties with null, undefined properties ', () => { expect( - countNestedProperties({ + buildObjectKeysArray({ a: null, b: undefined, c: 'value3', }) - ).toBe(3) + ).toStrictEqual(['a', 'b', 'c']) }) -test('should allow to count object with properties array property', () => { +test('should allow to build an array of object properties with properties array property', () => { expect( - countNestedProperties({ + buildObjectKeysArray({ a: [1, 2], b: true, c: true, }) - ).toBe(4) + ).toStrictEqual(['a.0', 'a.1', 'b', 'c']) }) -test('should allow to count object properties with empty array property', () => { +test('should allow to build an array of object properties with empty array property', () => { expect( - countNestedProperties({ + buildObjectKeysArray({ a: true, b: true, c: { @@ -78,7 +82,21 @@ test('should allow to count object properties with empty array property', () => e: [], }, }) - ).toBe(4) + ).toStrictEqual(['a', 'b', 'c.d', 'c.e']) +}) + +test('should allow to build an array of object properties from nested object', () => { + expect( + buildObjectKeysArray({ + a: true, + b: { + b1: true, + b2: true, + b3: {}, + }, + c: true, + }) + ).toStrictEqual(['a', 'b.b1', 'b.b2', 'b.b3', 'c']) }) test('object property is defined', () => { @@ -140,7 +158,12 @@ test('object property is not defined', () => { ) expect(() => assertObjectMatchSpec( - { name: 'john', gender: 'male', city: 'paris', street: 'rue du chat qui pĂȘche' }, + { + name: 'john', + gender: 'male', + city: 'paris', + street: 'rue du chat qui pĂȘche', + }, spec ) ).toThrow(`Property 'name' is defined: expected 'john' to be undefined`) @@ -227,13 +250,23 @@ test('check object property does not contain value', () => { expect(() => assertObjectMatchSpec( - { first_name: 'foo', last_name: 'bar', city: 'miami', street: 'calle ocho' }, + { + first_name: 'foo', + last_name: 'bar', + city: 'miami', + street: 'calle ocho', + }, spec ) ).not.toThrow() expect(() => assertObjectMatchSpec( - { first_name: 'johnny', last_name: 'bar', city: 'miami', street: 'calle ocho' }, + { + first_name: 'johnny', + last_name: 'bar', + city: 'miami', + street: 'calle ocho', + }, spec ) ).toThrow( @@ -241,13 +274,23 @@ test('check object property does not contain value', () => { ) expect(() => assertObjectMatchSpec( - { first_name: 'foo', last_name: 'doet', city: 'miami', street: 'calle ocho' }, + { + first_name: 'foo', + last_name: 'doet', + city: 'miami', + street: 'calle ocho', + }, spec ) ).toThrow(`Property 'last_name' (doet) contains 'doe': expected 'doet' to not include 'doe'`) expect(() => assertObjectMatchSpec( - { first_name: 'foo', last_name: 'bar', city: 'new york', street: 'calle ocho' }, + { + first_name: 'foo', + last_name: 'bar', + city: 'new york', + street: 'calle ocho', + }, spec ) ).toThrow( @@ -255,7 +298,12 @@ test('check object property does not contain value', () => { ) expect(() => assertObjectMatchSpec( - { first_name: 'foo', last_name: 'bar', city: 'miami', street: 'krome avenue' }, + { + first_name: 'foo', + last_name: 'bar', + city: 'miami', + street: 'krome avenue', + }, spec ) ).toThrow( @@ -303,36 +351,6 @@ test('check object property does not match regexp', () => { ) }) -test('check object fully matches spec', () => { - const spec = [ - { - field: 'first_name', - matcher: 'equal', - value: 'john', - }, - { - field: 'last_name', - matcher: 'match', - value: '^doe', - }, - ] - - expect(() => - assertObjectMatchSpec({ first_name: 'john', last_name: 'doet' }, spec, true) - ).not.toThrow() - expect(() => - assertObjectMatchSpec({ first_name: 'john', last_name: 'doet', gender: 'male' }, spec, true) - ).toThrow(`Expected json response to fully match spec, but it does not: expected 3 to equal 2`) - expect(() => - assertObjectMatchSpec({ first_name: 'john', last_name: 'john' }, spec, true) - ).toThrow(`Property 'last_name' (john) does not match '^doe': expected 'john' to match /^doe/`) - expect(() => - assertObjectMatchSpec({ first_name: 'doe', last_name: 'doe' }, spec, true) - ).toThrow( - `Expected property 'first_name' to equal 'john', but found 'doe': expected 'doe' to deeply equal 'john'` - ) -}) - test('check object property type', () => { const spec = [ { @@ -531,3 +549,69 @@ test('check unsupported matcher should fail', () => { `Matcher "unknown" did not match any supported assertions` ) }) + +test('check object fully matches spec', () => { + const spec = [ + { + field: 'first_name', + matcher: 'equal', + value: 'john', + }, + { + field: 'last_name', + matcher: 'match', + value: '^doe', + }, + { + field: 'address', + matcher: 'type', + value: 'object', + }, + { + field: 'phone.mobile', + matcher: 'match', + value: '^06', + }, + { + field: 'phone.mobile', + matcher: 'type', + value: 'string', + }, + ] + + expect(() => + assertObjectMatchSpec( + { + first_name: 'john', + last_name: 'doet', + address: {}, + phone: { mobile: '0600000000' }, + }, + spec, + true + ) + ).not.toThrow() + expect(() => + assertObjectMatchSpec( + { + first_name: 'john', + last_name: 'doet', + gender: 'male', + address: {}, + phone: { mobile: '0600000000' }, + }, + spec, + true + ) + ).toThrow( + `Expected json response to fully match spec, but it does not: expected [ Array(5) ] to deeply equal [ Array(4) ]` + ) + expect(() => + assertObjectMatchSpec({ first_name: 'john', last_name: 'john' }, spec, true) + ).toThrow(`Property 'last_name' (john) does not match '^doe': expected 'john' to match /^doe/`) + expect(() => + assertObjectMatchSpec({ first_name: 'doe', last_name: 'doe' }, spec, true) + ).toThrow( + `Expected property 'first_name' to equal 'john', but found 'doe': expected 'doe' to deeply equal 'john'` + ) +}) diff --git a/tests/extensions/http_api/definitions.test.js b/tests/extensions/http_api/definitions.test.js index 2ef46a29..291f2bab 100644 --- a/tests/extensions/http_api/definitions.test.js +++ b/tests/extensions/http_api/definitions.test.js @@ -744,7 +744,7 @@ test('check json response fully matches spec', () => { expect(() => def.exec(clientMock, 'fully ', { hashes: () => spec })).not.toThrow() expect(() => def.exec(clientMock, 'fully ', { hashes: () => spec })).toThrow( - `Expected json response to fully match spec, but it does not: expected 3 to equal 2` + `Expected json response to fully match spec, but it does not: expected [ 'first_name', 'last_name', 'gender' ] to deeply equal [ 'first_name', 'last_name' ]` ) expect(() => def.exec(clientMock, 'fully ', { hashes: () => spec })).toThrow( `Property 'last_name' (be) does not match 'ben': expected 'be' to match /ben/`