Skip to content

Commit 0bb0628

Browse files
committed
feat(assertObjectMatchSpec): Compare object keys when doing a full match #44
1 parent 3ebe10a commit 0bb0628

File tree

3 files changed

+166
-73
lines changed

3 files changed

+166
-73
lines changed

src/core/assertions.js

+27-18
Original file line numberDiff line numberDiff line change
@@ -28,41 +28,48 @@ const RuleName = Object.freeze({
2828
})
2929

3030
/**
31-
* Count object properties including nested objects ones.
32-
* If a property is an object, its key is ignored.
31+
* Make an array of keys from an object.
32+
* Use a dot notation
33+
* Only empty object keys are taken into account
34+
* For subobjects only keys are taken into account
3335
*
3436
* @example
35-
* Assertions.countNestedProperties({
37+
* Assertions.buildObjectKeysArray({
3638
* a: true,
3739
* b: true,
3840
* c: true,
3941
* })
40-
* // => 3
41-
* Assertions.countNestedProperties({
42+
* // => ["a", "b", "c"]
43+
* Assertions.buildObjectKeysArray({
4244
* a: true,
4345
* b: true,
4446
* c: {
45-
* a: true,
46-
* b: true,
47+
* d: true,
48+
* e: {},
49+
* f: {
50+
* g: true
51+
* }
4752
* },
4853
* })
49-
* // => 4 (c is ignored because it's a nested object)
54+
* // => ["a", "b", "c.d", "c.e", "c.f.g"] (c and c.f are ignored as non empty nested objects)
5055
*
5156
* @param {Object} object
5257
* @return {number}
5358
*/
54-
exports.countNestedProperties = (object) => {
55-
let propertiesCount = 0
59+
exports.buildObjectKeysArray = (object, dottedObjectKeys = [], currentPath = '') => {
5660
Object.keys(object).forEach((key) => {
5761
if (!_.isEmpty(object[key]) && typeof object[key] === 'object') {
58-
const count = exports.countNestedProperties(object[key])
59-
propertiesCount += count
62+
dottedObjectKeys = exports.buildObjectKeysArray(
63+
object[key],
64+
dottedObjectKeys,
65+
`${currentPath}${key}.`
66+
)
6067
} else {
61-
propertiesCount++
68+
dottedObjectKeys.push(`${currentPath}${key}`)
6269
}
6370
})
6471

65-
return propertiesCount
72+
return dottedObjectKeys
6673
}
6774

6875
/**
@@ -106,10 +113,11 @@ exports.countNestedProperties = (object) => {
106113
* @param {boolean} [exact=false] - if `true`, specification must match all object's properties
107114
*/
108115
exports.assertObjectMatchSpec = (object, spec, exact = false) => {
116+
const specPath = new Set()
109117
spec.forEach(({ field, matcher, value }) => {
110118
const currentValue = _.get(object, field)
111119
const expectedValue = Cast.value(value)
112-
120+
specPath.add(field)
113121
const rule = exports.getMatchingRule(matcher)
114122

115123
switch (rule.name) {
@@ -208,11 +216,12 @@ exports.assertObjectMatchSpec = (object, spec, exact = false) => {
208216

209217
// We check we have exactly the same number of properties as expected
210218
if (exact === true) {
211-
const propertiesCount = exports.countNestedProperties(object)
219+
const objectKeys = exports.buildObjectKeysArray(object)
220+
const specObjectKeys = Array.from(specPath)
212221
expect(
213-
propertiesCount,
222+
objectKeys,
214223
'Expected json response to fully match spec, but it does not'
215-
).to.be.equal(spec.length)
224+
).to.be.deep.equal(specObjectKeys)
216225
}
217226
}
218227

tests/core/assertions.test.js

+138-54
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
'use strict'
22

3-
const { countNestedProperties, assertObjectMatchSpec } = require('../../src/core/assertions')
3+
const {
4+
countNestedProperties,
5+
assertObjectMatchSpec,
6+
buildObjectKeysArray,
7+
} = require('../../src/core/assertions')
48

59
beforeAll(() => {
610
const MockDate = (lastDate) => () => new lastDate(2018, 4, 1)
@@ -13,17 +17,17 @@ afterAll(() => {
1317

1418
beforeEach(() => {})
1519

16-
test('should allow to count object properties', () => {
20+
test('should allow to build an array of object properties', () => {
1721
expect(
18-
countNestedProperties({
22+
buildObjectKeysArray({
1923
a: true,
2024
b: true,
2125
c: true,
2226
})
23-
).toBe(3)
27+
).toStrictEqual(['a', 'b', 'c'])
2428

2529
expect(
26-
countNestedProperties({
30+
buildObjectKeysArray({
2731
a: true,
2832
b: true,
2933
c: true,
@@ -32,53 +36,67 @@ test('should allow to count object properties', () => {
3236
b: true,
3337
},
3438
})
35-
).toBe(5)
39+
).toStrictEqual(['a', 'b', 'c', 'd.a', 'd.b'])
3640
})
3741

38-
test('should allow to count nested objects properties', () => {
42+
test('should allow to build an array of object propertieswith nested objects properties', () => {
3943
expect(
40-
countNestedProperties({
44+
buildObjectKeysArray({
4145
a: true,
4246
b: true,
4347
c: {
4448
d: 'value1',
4549
e: 'value2',
4650
},
4751
})
48-
).toBe(4)
52+
).toStrictEqual(['a', 'b', 'c.d', 'c.e'])
4953
})
5054

51-
test('should allow to count object properties with null, undefined properties ', () => {
55+
test('should allow to build an array of object properties with null, undefined properties ', () => {
5256
expect(
53-
countNestedProperties({
57+
buildObjectKeysArray({
5458
a: null,
5559
b: undefined,
5660
c: 'value3',
5761
})
58-
).toBe(3)
62+
).toStrictEqual(['a', 'b', 'c'])
5963
})
6064

61-
test('should allow to count object with properties array property', () => {
65+
test('should allow to build an array of object properties with properties array property', () => {
6266
expect(
63-
countNestedProperties({
67+
buildObjectKeysArray({
6468
a: [1, 2],
6569
b: true,
6670
c: true,
6771
})
68-
).toBe(4)
72+
).toStrictEqual(['a.0', 'a.1', 'b', 'c'])
6973
})
7074

71-
test('should allow to count object properties with empty array property', () => {
75+
test('should allow to build an array of object properties with empty array property', () => {
7276
expect(
73-
countNestedProperties({
77+
buildObjectKeysArray({
7478
a: true,
7579
b: true,
7680
c: {
7781
d: '',
7882
e: [],
7983
},
8084
})
81-
).toBe(4)
85+
).toStrictEqual(['a', 'b', 'c.d', 'c.e'])
86+
})
87+
88+
test('should allow to build an array of object properties from nested object', () => {
89+
expect(
90+
buildObjectKeysArray({
91+
a: true,
92+
b: {
93+
b1: true,
94+
b2: true,
95+
b3: {},
96+
},
97+
c: true,
98+
})
99+
).toStrictEqual(['a', 'b.b1', 'b.b2', 'b.b3', 'c'])
82100
})
83101

84102
test('object property is defined', () => {
@@ -140,7 +158,12 @@ test('object property is not defined', () => {
140158
)
141159
expect(() =>
142160
assertObjectMatchSpec(
143-
{ name: 'john', gender: 'male', city: 'paris', street: 'rue du chat qui pêche' },
161+
{
162+
name: 'john',
163+
gender: 'male',
164+
city: 'paris',
165+
street: 'rue du chat qui pêche',
166+
},
144167
spec
145168
)
146169
).toThrow(`Property 'name' is defined: expected 'john' to be undefined`)
@@ -227,35 +250,60 @@ test('check object property does not contain value', () => {
227250

228251
expect(() =>
229252
assertObjectMatchSpec(
230-
{ first_name: 'foo', last_name: 'bar', city: 'miami', street: 'calle ocho' },
253+
{
254+
first_name: 'foo',
255+
last_name: 'bar',
256+
city: 'miami',
257+
street: 'calle ocho',
258+
},
231259
spec
232260
)
233261
).not.toThrow()
234262
expect(() =>
235263
assertObjectMatchSpec(
236-
{ first_name: 'johnny', last_name: 'bar', city: 'miami', street: 'calle ocho' },
264+
{
265+
first_name: 'johnny',
266+
last_name: 'bar',
267+
city: 'miami',
268+
street: 'calle ocho',
269+
},
237270
spec
238271
)
239272
).toThrow(
240273
`Property 'first_name' (johnny) contains 'john': expected 'johnny' to not include 'john'`
241274
)
242275
expect(() =>
243276
assertObjectMatchSpec(
244-
{ first_name: 'foo', last_name: 'doet', city: 'miami', street: 'calle ocho' },
277+
{
278+
first_name: 'foo',
279+
last_name: 'doet',
280+
city: 'miami',
281+
street: 'calle ocho',
282+
},
245283
spec
246284
)
247285
).toThrow(`Property 'last_name' (doet) contains 'doe': expected 'doet' to not include 'doe'`)
248286
expect(() =>
249287
assertObjectMatchSpec(
250-
{ first_name: 'foo', last_name: 'bar', city: 'new york', street: 'calle ocho' },
288+
{
289+
first_name: 'foo',
290+
last_name: 'bar',
291+
city: 'new york',
292+
street: 'calle ocho',
293+
},
251294
spec
252295
)
253296
).toThrow(
254297
`Property 'city' (new york) contains 'york': expected 'new york' to not include 'york'`
255298
)
256299
expect(() =>
257300
assertObjectMatchSpec(
258-
{ first_name: 'foo', last_name: 'bar', city: 'miami', street: 'krome avenue' },
301+
{
302+
first_name: 'foo',
303+
last_name: 'bar',
304+
city: 'miami',
305+
street: 'krome avenue',
306+
},
259307
spec
260308
)
261309
).toThrow(
@@ -303,36 +351,6 @@ test('check object property does not match regexp', () => {
303351
)
304352
})
305353

306-
test('check object fully matches spec', () => {
307-
const spec = [
308-
{
309-
field: 'first_name',
310-
matcher: 'equal',
311-
value: 'john',
312-
},
313-
{
314-
field: 'last_name',
315-
matcher: 'match',
316-
value: '^doe',
317-
},
318-
]
319-
320-
expect(() =>
321-
assertObjectMatchSpec({ first_name: 'john', last_name: 'doet' }, spec, true)
322-
).not.toThrow()
323-
expect(() =>
324-
assertObjectMatchSpec({ first_name: 'john', last_name: 'doet', gender: 'male' }, spec, true)
325-
).toThrow(`Expected json response to fully match spec, but it does not: expected 3 to equal 2`)
326-
expect(() =>
327-
assertObjectMatchSpec({ first_name: 'john', last_name: 'john' }, spec, true)
328-
).toThrow(`Property 'last_name' (john) does not match '^doe': expected 'john' to match /^doe/`)
329-
expect(() =>
330-
assertObjectMatchSpec({ first_name: 'doe', last_name: 'doe' }, spec, true)
331-
).toThrow(
332-
`Expected property 'first_name' to equal 'john', but found 'doe': expected 'doe' to deeply equal 'john'`
333-
)
334-
})
335-
336354
test('check object property type', () => {
337355
const spec = [
338356
{
@@ -531,3 +549,69 @@ test('check unsupported matcher should fail', () => {
531549
`Matcher "unknown" did not match any supported assertions`
532550
)
533551
})
552+
553+
test('check object fully matches spec', () => {
554+
const spec = [
555+
{
556+
field: 'first_name',
557+
matcher: 'equal',
558+
value: 'john',
559+
},
560+
{
561+
field: 'last_name',
562+
matcher: 'match',
563+
value: '^doe',
564+
},
565+
{
566+
field: 'address',
567+
matcher: 'type',
568+
value: 'object',
569+
},
570+
{
571+
field: 'phone.mobile',
572+
matcher: 'match',
573+
value: '^06',
574+
},
575+
{
576+
field: 'phone.mobile',
577+
matcher: 'type',
578+
value: 'string',
579+
},
580+
]
581+
582+
expect(() =>
583+
assertObjectMatchSpec(
584+
{
585+
first_name: 'john',
586+
last_name: 'doet',
587+
address: {},
588+
phone: { mobile: '0600000000' },
589+
},
590+
spec,
591+
true
592+
)
593+
).not.toThrow()
594+
expect(() =>
595+
assertObjectMatchSpec(
596+
{
597+
first_name: 'john',
598+
last_name: 'doet',
599+
gender: 'male',
600+
address: {},
601+
phone: { mobile: '0600000000' },
602+
},
603+
spec,
604+
true
605+
)
606+
).toThrow(
607+
`Expected json response to fully match spec, but it does not: expected [ Array(5) ] to deeply equal [ Array(4) ]`
608+
)
609+
expect(() =>
610+
assertObjectMatchSpec({ first_name: 'john', last_name: 'john' }, spec, true)
611+
).toThrow(`Property 'last_name' (john) does not match '^doe': expected 'john' to match /^doe/`)
612+
expect(() =>
613+
assertObjectMatchSpec({ first_name: 'doe', last_name: 'doe' }, spec, true)
614+
).toThrow(
615+
`Expected property 'first_name' to equal 'john', but found 'doe': expected 'doe' to deeply equal 'john'`
616+
)
617+
})

0 commit comments

Comments
 (0)