Skip to content
This repository was archived by the owner on Nov 8, 2024. It is now read-only.

Commit cc1825f

Browse files
Merge pull request #180 from apiaryio/179-real-coercion
fix: uses strict and weak coercion for real and expected messages
2 parents 74e465e + d23263b commit cc1825f

File tree

8 files changed

+243
-63
lines changed

8 files changed

+243
-63
lines changed

lib/next/test/integration/validateMessage.test.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,4 +361,104 @@ describe('validateMessage', () => {
361361
});
362362
});
363363
});
364+
365+
describe('with non-matching headers', () => {
366+
const result = validateMessage(
367+
{
368+
statusCode: 404
369+
},
370+
{
371+
statusCode: 404,
372+
headers: {
373+
'Content-Type': 'text/plain'
374+
}
375+
}
376+
);
377+
378+
it('returns validation result object', () => {
379+
assert.isObject(result);
380+
});
381+
382+
it('contains all validatable keys', () => {
383+
assert.hasAllDeepKeys(result, ['isValid', 'statusCode', 'headers']);
384+
});
385+
386+
it('has "isValid" as false', () => {
387+
assert.propertyVal(result, 'isValid', false);
388+
});
389+
390+
describe('statusCode', () => {
391+
it('has "TextDiff" validator', () => {
392+
assert.propertyVal(result.statusCode, 'validator', 'TextDiff');
393+
});
394+
395+
it('has "text/vnd.apiary.status-code" real type', () => {
396+
assert.propertyVal(
397+
result.statusCode,
398+
'realType',
399+
'text/vnd.apiary.status-code'
400+
);
401+
});
402+
403+
it('has "text/vnd.apiary.status-code" expected type', () => {
404+
assert.propertyVal(
405+
result.statusCode,
406+
'expectedType',
407+
'text/vnd.apiary.status-code'
408+
);
409+
});
410+
411+
it('has no errors', () => {
412+
assert.lengthOf(result.statusCode.results, 0);
413+
});
414+
});
415+
416+
describe('headers', () => {
417+
it('has "HeadersJsonExample" validator', () => {
418+
assert.propertyVal(result.headers, 'validator', 'HeadersJsonExample');
419+
});
420+
421+
it('has "application/vnd.apiary.http-headers+json" real type', () => {
422+
assert.propertyVal(
423+
result.headers,
424+
'realType',
425+
'application/vnd.apiary.http-headers+json'
426+
);
427+
});
428+
429+
it('has "application/vnd.apiary.http-headers+json" expected type', () => {
430+
assert.propertyVal(
431+
result.headers,
432+
'expectedType',
433+
'application/vnd.apiary.http-headers+json'
434+
);
435+
});
436+
437+
describe('produces an error', () => {
438+
it('exactly one error', () => {
439+
assert.lengthOf(result.headers.results, 1);
440+
});
441+
442+
it('has "error" severity', () => {
443+
assert.propertyVal(result.headers.results[0], 'severity', 'error');
444+
});
445+
446+
it('has pointer to missing "Content-Type"', () => {
447+
assert.propertyVal(
448+
result.headers.results[0],
449+
'pointer',
450+
'/content-type'
451+
);
452+
});
453+
454+
it('has explanatory message', () => {
455+
assert.propertyVal(
456+
result.headers.results[0],
457+
'message',
458+
`At '/content-type' Missing required property: content-type`
459+
);
460+
});
461+
});
462+
});
463+
});
364464
});

lib/next/test/unit/units/normalize/normalizeHeaders.test.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@ const {
55

66
describe('normalizeHeaders', () => {
77
describe('when given nothing', () => {
8-
const headers = normalizeHeaders(undefined);
9-
10-
it('coerces to empty object', () => {
11-
assert.deepEqual(headers, {});
8+
it('throws upon invalid headers value', () => {
9+
assert.throw(() => normalizeHeaders(undefined));
1210
});
1311
});
1412

lib/next/test/unit/utils/evolve.test.js

Lines changed: 84 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,71 +11,108 @@ const unexpectedTypes = [
1111
];
1212

1313
describe('evolve', () => {
14-
describe('evolves a given object', () => {
15-
const res = evolve({
16-
a: multiply(2),
17-
c: () => null
18-
})({
19-
a: 2,
20-
b: 'foo'
21-
});
14+
describe('weak mode', () => {
15+
describe('evolves given object', () => {
16+
const result = evolve({
17+
a: multiply(2),
18+
c: () => null
19+
})({
20+
a: 2,
21+
b: 'foo'
22+
});
2223

23-
it('returns object', () => {
24-
assert.isObject(res);
25-
});
24+
it('returns object', () => {
25+
assert.isObject(result);
26+
});
2627

27-
it('evolves matching properties', () => {
28-
assert.propertyVal(res, 'a', 4);
29-
});
28+
it('evolves matching properties', () => {
29+
assert.propertyVal(result, 'a', 4);
30+
});
3031

31-
it('bypasses properties not in schema', () => {
32-
assert.propertyVal(res, 'b', 'foo');
33-
});
32+
it('bypasses properties not in schema', () => {
33+
assert.propertyVal(result, 'b', 'foo');
34+
});
3435

35-
it('ignores properties not in data', () => {
36-
assert.notProperty(res, 'c');
36+
it('ignores properties not in data', () => {
37+
assert.notProperty(result, 'c');
38+
});
3739
});
38-
});
3940

40-
describe('evolves a given array', () => {
41-
const res = evolve({
42-
0: multiply(2),
43-
1: multiply(3),
44-
3: multiply(4)
45-
})([1, 2, 3]);
41+
describe('evolves given array', () => {
42+
const result = evolve({
43+
0: multiply(2),
44+
1: multiply(3),
45+
3: multiply(4)
46+
})([1, 2, 3]);
4647

47-
it('returns array', () => {
48-
assert.isArray(res);
49-
});
48+
it('returns array', () => {
49+
assert.isArray(result);
50+
});
5051

51-
it('evolves matching keys', () => {
52-
assert.propertyVal(res, 0, 2);
53-
assert.propertyVal(res, 1, 6);
54-
});
52+
it('evolves matching properties', () => {
53+
assert.propertyVal(result, 0, 2);
54+
assert.propertyVal(result, 1, 6);
55+
});
5556

56-
it('bypasses keys not in schema', () => {
57-
assert.propertyVal(res, 2, 3);
57+
it('bypasses properties not in schema', () => {
58+
assert.propertyVal(result, 2, 3);
59+
});
60+
61+
it('ignores properties not in data', () => {
62+
assert.notProperty(result, 3);
63+
});
5864
});
5965

60-
it('ignores properties not in data', () => {
61-
assert.notProperty(res, 3);
66+
describe('throws when given unexpected schema', () => {
67+
unexpectedTypes
68+
.concat([['array', [1, 2]]])
69+
.forEach(([typeName, dataType]) => {
70+
it(`when given ${typeName}`, () => {
71+
assert.throw(() => evolve(dataType)({}));
72+
});
73+
});
6274
});
63-
});
6475

65-
describe('throws when given unexpected schema', () => {
66-
unexpectedTypes
67-
.concat([['array', [1, 2]]])
68-
.forEach(([typeName, dataType]) => {
76+
describe('throws when given unexpected data', () => {
77+
unexpectedTypes.forEach(([typeName, dataType]) => {
6978
it(`when given ${typeName}`, () => {
70-
assert.throw(() => evolve(dataType)({}));
79+
assert.throw(() => evolve({ a: () => null })(dataType));
7180
});
7281
});
82+
});
7383
});
7484

75-
describe('throws when given unexpected data', () => {
76-
unexpectedTypes.forEach(([typeName, dataType]) => {
77-
it(`when given ${typeName}`, () => {
78-
assert.throw(() => evolve({ a: () => null })(dataType));
85+
describe('strict mode', () => {
86+
describe('evolves given object', () => {
87+
const result = evolve(
88+
{
89+
method: () => 'GET',
90+
headers: () => 'foo',
91+
body: multiply(2)
92+
},
93+
{
94+
strict: true
95+
}
96+
)({
97+
statusCode: 200,
98+
body: 5
99+
});
100+
101+
it('returns an object', () => {
102+
assert.isObject(result);
103+
});
104+
105+
it('evolves matching properties', () => {
106+
assert.propertyVal(result, 'body', 10);
107+
});
108+
109+
it('forces all properties from schema', () => {
110+
assert.propertyVal(result, 'headers', 'foo');
111+
assert.propertyVal(result, 'method', 'GET');
112+
});
113+
114+
it('bypasses properties not present in schema', () => {
115+
assert.propertyVal(result, 'statusCode', 200);
79116
});
80117
});
81118
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Coerces given headers to an empty Object in case not present.
2+
// Conceptually, diff between missing headers and empty headers
3+
// should be treated the same.
4+
const coerceHeaders = (headers) => {
5+
return headers || {};
6+
};
7+
8+
module.exports = { coerceHeaders };

lib/next/units/coerce/index.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const evolve = require('../../utils/evolve');
2+
const { coerceHeaders } = require('./coerceHeaders');
3+
4+
const coercionMap = {
5+
headers: coerceHeaders
6+
};
7+
8+
// Coercion uses strict evolve to ensure the properties
9+
// set in expected schema are set on the result object,
10+
// even if not present in data object. This is what
11+
// coercion is about, in the end.
12+
const coerce = evolve(coercionMap, { strict: true });
13+
const coerceWeak = evolve(coercionMap);
14+
15+
module.exports = { coerce, coerceWeak };

lib/next/units/normalize/normalizeHeaders.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@ const normalizeStringValue = (value) => {
88
* @returns {Object}
99
*/
1010
const normalizeHeaders = (headers) => {
11-
if (!headers) {
12-
return {};
13-
}
14-
1511
const headersType = typeof headers;
1612
const isHeadersNull = headers == null;
1713

lib/next/utils/evolve.js

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
* Applies a given evolution schema to the given data Object.
33
* Properties not present in schema are bypassed.
44
* Properties not present in data are ignored.
5-
* @param {Object} schema
6-
* @param {Object|Array} data
7-
* @returns {Object}
5+
* @param {Object<string, any>} schema
6+
* @param {any[]|Object<string, any>} data
7+
* @returns {any[]|Object<string, any>}
88
*/
9-
const evolve = (schema) => (data) => {
9+
const evolve = (schema, { strict = false } = {}) => (data) => {
1010
const dataType = typeof data;
1111
const schemaType = typeof schema;
1212
const isArray = Array.isArray(data);
@@ -24,10 +24,11 @@ const evolve = (schema) => (data) => {
2424
);
2525
}
2626

27-
return Object.keys(data).reduce((acc, key) => {
27+
const reducer = (acc, key) => {
2828
const value = data[key];
2929
const transform = schema[key];
3030
const transformType = typeof transform;
31+
3132
/* eslint-disable no-nested-ternary */
3233
const nextValue =
3334
transformType === 'function'
@@ -38,7 +39,21 @@ const evolve = (schema) => (data) => {
3839
/* eslint-enable no-nested-ternary */
3940

4041
return isArray ? acc.concat(nextValue) : { ...acc, [key]: nextValue };
41-
}, result);
42+
};
43+
44+
const nextData = Object.keys(data).reduce(reducer, result);
45+
46+
if (strict) {
47+
// Strict mode ensures all keys in expected schema are present
48+
// in the returned payload.
49+
return Object.keys(schema)
50+
.filter((expectedKey) => {
51+
return !Object.prototype.hasOwnProperty.call(data, expectedKey);
52+
})
53+
.reduce(reducer, nextData);
54+
}
55+
56+
return nextData;
4257
};
4358

4459
module.exports = evolve;

lib/next/validateMessage.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
const isset = require('../utils/isset');
2+
const { coerce, coerceWeak } = require('./units/coerce');
23
const { normalize } = require('./units/normalize');
34
const { isValid } = require('./units/isValid');
45
const { validateStatusCode } = require('./units/validateStatusCode');
56
const { validateHeaders } = require('./units/validateHeaders');
67
const { validateBody } = require('./units/validateBody');
78

89
function validateMessage(realMessage, expectedMessage) {
9-
const real = normalize(realMessage);
10-
const expected = normalize(expectedMessage);
1110
const results = {};
1211

12+
// Uses strict coercion on real message.
13+
// Strict coercion ensures real message has properties illegible
14+
// for validation with the expected message.
15+
const real = normalize(coerce(realMessage));
16+
17+
// Weak coercion applies transformation only to the properties
18+
// present in the given message. We don't want to mutate user's assertion.
19+
// However, we do want to use the same coercion logic we do
20+
// for strict coercion. Thus normalization and coercion are separate.
21+
const expected = normalize(coerceWeak(expectedMessage));
22+
1323
if (real.statusCode) {
1424
results.statusCode = validateStatusCode(real, expected);
1525
}
@@ -25,6 +35,7 @@ function validateMessage(realMessage, expectedMessage) {
2535
results.body = validateBody(real, expected);
2636
}
2737

38+
// Indicates the validity of the real message
2839
results.isValid = isValid(results);
2940

3041
return results;

0 commit comments

Comments
 (0)