Skip to content

Commit

Permalink
feat(assertObjectMatchSpec): Compare object keys when doing a full ma…
Browse files Browse the repository at this point in the history
…tch #44
  • Loading branch information
leguellec committed Dec 18, 2020
1 parent 3ebe10a commit 0bb0628
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 73 deletions.
45 changes: 27 additions & 18 deletions src/core/assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
}

Expand Down
192 changes: 138 additions & 54 deletions tests/core/assertions.test.js
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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,
Expand All @@ -32,53 +36,67 @@ 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: {
d: 'value1',
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: {
d: '',
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', () => {
Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -227,35 +250,60 @@ 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(
`Property 'first_name' (johnny) contains 'john': expected 'johnny' to not include 'john'`
)
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(
`Property 'city' (new york) contains 'york': expected 'new york' to not include 'york'`
)
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(
Expand Down Expand Up @@ -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 = [
{
Expand Down Expand Up @@ -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'`
)
})
Loading

0 comments on commit 0bb0628

Please sign in to comment.