diff --git a/lib/units/validateBody.js b/lib/units/validateBody.js index f1935e3f..464f05c2 100644 --- a/lib/units/validateBody.js +++ b/lib/units/validateBody.js @@ -2,7 +2,7 @@ const mediaTyper = require('media-typer'); const contentTypeUtils = require('content-type'); const { isValidField } = require('./isValid'); -const { TextDiff, JsonExample, JsonSchema } = require('../validators'); +const { TextDiff, JsonExample, JsonSchemaValidator } = require('../validators'); const isset = require('../utils/isset'); const parseJson = require('../utils/parseJson'); @@ -132,7 +132,7 @@ function getBodyValidator(expectedType, actualType) { // List JsonSchema first, because weak predicate of JsonExample // would resolve on "application/schema+json" media type too. [ - JsonSchema, + JsonSchemaValidator, (expected, actual) => { return isJson(actual) && isJsonSchema(expected); }, @@ -235,7 +235,8 @@ function validateBody(expected, actual) { } } - const usesJsonSchema = ValidatorClass && ValidatorClass.name === 'JsonSchema'; + const usesJsonSchema = + ValidatorClass && ValidatorClass.name === 'JsonSchemaValidator'; const validator = ValidatorClass && new ValidatorClass(usesJsonSchema ? expected.bodySchema : expected.body); diff --git a/lib/utils/schema-v4-generator.js b/lib/utils/schema-v4-generator.js index 36ddf507..3e5164d3 100644 --- a/lib/utils/schema-v4-generator.js +++ b/lib/utils/schema-v4-generator.js @@ -90,11 +90,11 @@ class SchemaV4Generator { if (firstLevel) { schemaDict.$schema = SCHEMA_VERSION; - schemaDict.id = '#'; + schemaDict.$id = '#'; } if (objectId != null) { - schemaDict.id = objectId; + schemaDict.$id = objectId; } const schemaType = this.getSchemaTypeFor(baseObject); diff --git a/lib/validators/headers-json-example.js b/lib/validators/headers-json-example.js index 42d56b76..0e42915a 100644 --- a/lib/validators/headers-json-example.js +++ b/lib/validators/headers-json-example.js @@ -1,5 +1,5 @@ const errors = require('../errors'); -const { JsonSchemaLegacy } = require('./json-schema-legacy'); +const { JsonSchemaValidator } = require('./json-schema-next'); const { SchemaV4Generator, SchemaV4Properties @@ -34,7 +34,7 @@ const getSchema = (json) => { return schemaGenerator.generate(); }; -class HeadersJsonExample extends JsonSchemaLegacy { +class HeadersJsonExample extends JsonSchemaValidator { constructor(expected) { if (typeof expected !== 'object') { throw new errors.MalformedDataError('Expected is not an Object'); diff --git a/lib/validators/index.js b/lib/validators/index.js index caee105f..8cc216ac 100644 --- a/lib/validators/index.js +++ b/lib/validators/index.js @@ -1,11 +1,11 @@ const { HeadersJsonExample } = require('./headers-json-example'); const { TextDiff } = require('./text-diff'); const { JsonExample } = require('./json-example'); -const { JsonSchema } = require('./json-schema'); +const { JsonSchemaValidator } = require('./json-schema-next'); module.exports = { HeadersJsonExample, TextDiff, JsonExample, - JsonSchema + JsonSchemaValidator }; diff --git a/lib/validators/json-example.js b/lib/validators/json-example.js index de18944c..be1810ab 100644 --- a/lib/validators/json-example.js +++ b/lib/validators/json-example.js @@ -1,11 +1,10 @@ -const errors = require('../errors'); -const { JsonSchema } = require('./json-schema'); +const { JsonSchemaValidator } = require('./json-schema-next'); const { SchemaV4Generator, SchemaV4Properties } = require('../utils/schema-v4-generator'); -function getSchema(json) { +function generateSchema(json) { const properties = new SchemaV4Properties({ keysStrict: false, valuesStrict: false, @@ -15,37 +14,16 @@ function getSchema(json) { return schemaGenerator.generate(); } -class JsonExample extends JsonSchema { +/** + * JSON Example validator is a superset of JSON Schema validation + * that generates a JSON Schema based on the given actual data. + */ +class JsonExample extends JsonSchemaValidator { constructor(expected) { // Generate JSON Schema from the given expected JSON data. - const jsonSchema = getSchema(expected); + const jsonSchema = generateSchema(expected); super(jsonSchema); } - - /** - * @throw {MalformedDataError} when actual is not a String or when no schema provided and expected is not a String - */ - validate(actual) { - const { jsonSchema: expected } = this; - - if (typeof actual !== 'string') { - const outError = new errors.MalformedDataError( - 'JsonExample validator: provided actual data is not string' - ); - outError.data = actual; - throw outError; - } - - if (typeof expected !== 'string') { - const outError = new errors.MalformedDataError( - 'JsonExample validator: provided expected data is not string' - ); - outError.data = expected; - throw outError; - } - - return super.validate(actual); - } } module.exports = { diff --git a/lib/validators/json-schema-legacy.js b/lib/validators/json-schema-legacy.js deleted file mode 100644 index 38fbaf79..00000000 --- a/lib/validators/json-schema-legacy.js +++ /dev/null @@ -1,76 +0,0 @@ -const tv4 = require('tv4'); -const jsonPointer = require('json-pointer'); - -const { JsonSchemaValidator, META_SCHEMA } = require('./json-schema-next'); -const { ValidationErrors } = require('./validation-errors'); -const toGavelResult = require('../utils/to-gavel-result'); - -class JsonSchemaLegacy extends JsonSchemaValidator { - validateSchema() { - const { jsonSchema, jsonMetaSchema } = this; - - // Set the default JSON Schema version if no explicit - // version is provided. - const metaSchema = jsonMetaSchema || META_SCHEMA.draftV4; - - tv4.reset(); - tv4.addSchema('', metaSchema); - tv4.addSchema(metaSchema.$schema, metaSchema); - const validationResult = tv4.validateResult(jsonSchema, metaSchema); - - return validationResult.valid; - } - - validate(data) { - const parsedData = this.parseData(data); - - switch (this.jsonSchemaVersion) { - case 'draftV4': - return this.validateUsingTV4(parsedData); - default: - throw new Error( - `Attempted to use JsonSchemaLegacy on non-legacy JSON Schema ${this.jsonSchemaVersion}!` - ); - } - } - - validateUsingTV4(data) { - const result = tv4.validateMultiple(data, this.jsonSchema); - const validationErrors = result.errors.concat(result.missing); - - const amandaCompatibleError = { - length: validationErrors.length, - errorMessages: {} - }; - - for (let index = 0; index < validationErrors.length; index++) { - const validationError = validationErrors[index]; - let error; - - if (validationError instanceof Error) { - error = validationError; - } else { - error = new Error('Missing schema'); - error.params = { key: validationError }; - error.dataPath = ''; - } - - const pathArray = jsonPointer - .parse(error.dataPath) - .concat(error.params.key || []); - const pointer = jsonPointer.compile(pathArray); - - amandaCompatibleError[index] = { - message: `At '${pointer}' ${error.message}`, - property: pathArray, - attributeValue: true, - validatorName: 'error' - }; - } - - const errors = new ValidationErrors(amandaCompatibleError); - return toGavelResult(errors); - } -} - -module.exports = { JsonSchemaLegacy }; diff --git a/lib/validators/json-schema-next.js b/lib/validators/json-schema-next.js index 50f3e8ba..905ecd07 100644 --- a/lib/validators/json-schema-next.js +++ b/lib/validators/json-schema-next.js @@ -20,6 +20,10 @@ const META_SCHEMA = { draftV7: metaSchemaV7 }; +const last = (list) => { + return list[list.length - 1]; +}; + /** * Returns a JSON Schema Draft version of the given JSON Schema. */ @@ -32,6 +36,7 @@ const getExplicitSchemaVersion = (jsonSchema) => { }; const getImplicitSchemaVersion = (jsonSchema) => { + // A single boolean value is a valid JSON Schema Draft 7 if (typeof jsonSchema === 'boolean') { return 'draftV7'; } @@ -66,8 +71,8 @@ const getSchemaVersion = (jsonSchema) => { class JsonSchemaValidator { constructor(jsonSchema) { - this.jsonSchema = jsonSchema; - this.jsonSchemaVersion = getSchemaVersion(jsonSchema); + this.jsonSchema = this.resolveJsonSchema(jsonSchema); + this.jsonSchemaVersion = getSchemaVersion(this.jsonSchema); if (this.jsonSchemaVersion == null) { const supportedVersions = Object.keys(SCHEMA_VERSIONS).join('/'); @@ -90,6 +95,28 @@ class JsonSchemaValidator { } } + /** + * Parses given JSON Schema string. + * Prevents invalid JSON to be consumed by a validator. + */ + resolveJsonSchema(jsonSchema) { + let resolvedJsonSchema = jsonSchema; + + if (typeof jsonSchema === 'string') { + try { + resolvedJsonSchema = parseJson(jsonSchema); + } catch (error) { + const unparsableJsonSchemaError = new errors.SchemaNotJsonParsableError( + `Given JSON Schema is not a valid JSON. ${error.message}` + ); + unparsableJsonSchemaError.schema = jsonSchema; + throw unparsableJsonSchemaError; + } + } + + return resolvedJsonSchema; + } + /** * Returns a meta schema for the provided JSON Schema. */ @@ -103,7 +130,11 @@ class JsonSchemaValidator { */ validateSchema() { const { jsonSchemaVersion, jsonSchema } = this; - const ajv = new Ajv(); + const ajv = new Ajv({ + // In order to use AJV with draft-4 and draft-6/7 + // provide the "auto" value to "schemaId". + schemaId: 'auto' + }); const metaSchema = META_SCHEMA[jsonSchemaVersion]; ajv.addMetaSchema(metaSchema, 'meta'); @@ -141,11 +172,18 @@ class JsonSchemaValidator { const parsedData = this.parseData(data); const ajv = new Ajv({ + // Enable for error messages to include enum violations. + allErrors: true, + // Enalbe verbose mode for error messages to include + // the actual values in `error[n].data`. + verbose: true, // Disable adding JSON Schema Draft 7 meta schema by default. // Allows to always add a meta schema depending on the schema version. meta: false, // No need to validate schema again, already validated - // in "validateSchema()" method. + // in "validateSchema()" method. Once AJV is the only validator + // it would make sense to remove the custom schema validation method + // and use this built-in behavior instead. validateSchema: false, jsonPointers: true }); @@ -154,13 +192,26 @@ class JsonSchemaValidator { // Convert AJV validation errors to the Gavel public validation errors. return (ajv.errors || []).map((ajvError) => { - const errorMessage = ajv.errorsText([ajvError]); - const { dataPath: pointer } = ajvError; + const relevantProperty = this.getErrorProperty(ajvError); - // When AJV does not return a property name - // compose it from the pointer. - const property = - ajvError.params.missingProperty || pointer.split('/').filter(Boolean); + const pointer = ajvError.dataPath.concat( + // Handle root-level pointers. + // AJV returns an empty `dataPath` when a root-level property + // rejects. TV4, however, used to return a pointer to a root-level + // property regardless. Keep backward-compatibility. + relevantProperty ? ['/', relevantProperty].join('') : '' + ); + + // Property is pretty much 1-1 representation of the pointer + // stored in the list of strings. + const property = pointer.split('/').filter(Boolean); + + const errorMessage = this.getBackwardCompatibleErrorMessage( + ajv, + ajvError, + pointer, + property + ); return { message: errorMessage, @@ -171,6 +222,42 @@ class JsonSchemaValidator { }; }); } + + getErrorProperty(ajvError) { + switch (ajvError.keyword) { + case 'required': + return ajvError.params.missingProperty; + case 'additionalProperties': + return ajvError.params.additionalProperty; + default: + return null; + } + } + + /** + * @deprecate + * Coerces AJV validation error message to the error message + * previously produced by Amanda/TV4 for backward-compatibility. + */ + getBackwardCompatibleErrorMessage(ajv, ajvError, pointer, property) { + const { keyword, data, params } = ajvError; + + switch (keyword) { + case 'type': + return `At '${pointer}' Invalid type: ${typeof data} (expected ${ + params.type + })`; + + case 'required': + return `At '${pointer}' Missing required property: ${last(property)}`; + + case 'enum': + return `At '${pointer}' No enum match for: "${data}"`; + + default: + return ajv.errorsText([ajvError]); + } + } } module.exports = { diff --git a/lib/validators/json-schema.js b/lib/validators/json-schema.js deleted file mode 100644 index 3f3a838b..00000000 --- a/lib/validators/json-schema.js +++ /dev/null @@ -1,45 +0,0 @@ -const { JsonSchemaLegacy } = require('./json-schema-legacy'); -const { JsonSchemaValidator, getSchemaVersion } = require('./json-schema-next'); -const errors = require('../errors'); -const parseJson = require('../utils/parseJson'); - -/** - * Resolve stringified JSON Schema to an Object. - * Validate invalid JSON Schema. - */ -function resolveJsonSchema(jsonSchema) { - let resolvedJsonSchema = jsonSchema; - - if (typeof jsonSchema === 'string') { - try { - resolvedJsonSchema = parseJson(jsonSchema); - } catch (error) { - const unparsableJsonSchemaError = new errors.SchemaNotJsonParsableError( - `Given JSON Schema is not a valid JSON. ${error.message}` - ); - unparsableJsonSchemaError.schema = jsonSchema; - throw unparsableJsonSchemaError; - } - } - - return resolvedJsonSchema; -} - -class JsonSchema { - constructor(jsonSchema) { - const resolvedJsonSchema = resolveJsonSchema(jsonSchema); - const jsonSchemaVersion = getSchemaVersion(resolvedJsonSchema); - const isLegacySchema = ['draftV4'].includes(jsonSchemaVersion); - - // Instantiate different JSON Schema validators - // based on the JSON Schema Draft version. - // Both validators have the same API. - return isLegacySchema - ? new JsonSchemaLegacy(resolvedJsonSchema) - : new JsonSchemaValidator(resolvedJsonSchema); - } -} - -module.exports = { - JsonSchema -}; diff --git a/test/cucumber/steps/fields.js b/test/cucumber/steps/fields.js index 7bb1978a..a33a05c9 100644 --- a/test/cucumber/steps/fields.js +++ b/test/cucumber/steps/fields.js @@ -9,7 +9,9 @@ module.exports = function() { fieldName, expectedJson ) { + const actual = this.result.fields[fieldName]; const expected = parseJson(expectedJson); - expect(this.result.fields[fieldName]).to.deep.equal(expected); + + expect(actual).to.deep.equal(expected); }); }; diff --git a/test/fixtures.js b/test/fixtures.js index 39aed636..5ab0dd19 100644 --- a/test/fixtures.js +++ b/test/fixtures.js @@ -259,7 +259,7 @@ const sampleJsonArrayItemMissing = ` const sampleJsonSchema = ` { "$schema": "http://json-schema.org/draft-04/schema#", - "id": "#", + "$id": "#", "required": [ "simple_key_value_pair", "complex_key_value_pair", @@ -271,45 +271,45 @@ const sampleJsonSchema = ` "type": "object", "properties": { "simple_key_value_pair": { - "id": "simple_key_value_pair", + "$id": "simple_key_value_pair", "enum": [ "simple_key_value_pair_value" ], "type": "string" }, "complex_key_value_pair": { - "id": "complex_key_value_pair", + "$id": "complex_key_value_pair", "additionalProperties": false, "type": "object", "properties": { "complex_key_value_pair_key1": { - "id": "complex_key_value_pair_key1", + "$id": "complex_key_value_pair_key1", "enum": [ "complex_key_value_pair_value1" ], "type": "string" }, "complex_key_value_pair_key2": { - "id": "complex_key_value_pair_key2", + "$id": "complex_key_value_pair_key2", "enum": [ "complex_key_value_pair_value2" ], "type": "string" }, "complex_key_value_pair_key3": { - "id": "complex_key_value_pair_key3", + "$id": "complex_key_value_pair_key3", "additionalProperties": false, "type": "object", "properties": { "complex_key_value_pair_key1_in_nested_hash": { - "id": "complex_key_value_pair_key1_in_nested_hash", + "$id": "complex_key_value_pair_key1_in_nested_hash", "enum": [ "complex_key_value_pair_value1_in_nested_hash" ], "type": "string" }, "complex_key_value_pair_key2_in_nested_hash": { - "id": "complex_key_value_pair_key2_in_nested_hash", + "$id": "complex_key_value_pair_key2_in_nested_hash", "enum": [ "complex_key_value_pair_value2_in_nested_hash" ], @@ -329,12 +329,12 @@ const sampleJsonSchema = ` ] }, "array_of_hashes": { - "id": "array_of_hashes", + "$id": "array_of_hashes", "additionalItems": false, "type": "array", "items": [ { - "id": "0", + "$id": "0", "required": [ "array_of_hashes_item1_key1", "array_of_hashes_item1_key2" @@ -343,14 +343,14 @@ const sampleJsonSchema = ` "type": "object", "properties": { "array_of_hashes_item1_key1": { - "id": "array_of_hashes_item1_key1", + "$id": "array_of_hashes_item1_key1", "enum": [ "array_of_hashes_item1_value1" ], "type": "string" }, "array_of_hashes_item1_key2": { - "id": "array_of_hashes_item1_key2", + "$id": "array_of_hashes_item1_key2", "enum": [ "array_of_hashes_item1_value2" ], @@ -359,7 +359,7 @@ const sampleJsonSchema = ` } }, { - "id": "1", + "$id": "1", "required": [ "array_of_hashes_item2_key1", "array_of_hashes_item2_key2" @@ -368,14 +368,14 @@ const sampleJsonSchema = ` "type": "object", "properties": { "array_of_hashes_item2_key1": { - "id": "array_of_hashes_item2_key1", + "$id": "array_of_hashes_item2_key1", "enum": [ "array_of_hashes_item2_value1" ], "type": "string" }, "array_of_hashes_item2_key2": { - "id": "array_of_hashes_item2_key2", + "$id": "array_of_hashes_item2_key2", "enum": [ "array_of_hashes_item2_value2" ], @@ -386,33 +386,33 @@ const sampleJsonSchema = ` ] }, "array_of_mixed_simple_types": { - "id": "array_of_mixed_simple_types", + "$id": "array_of_mixed_simple_types", "additionalItems": false, "type": "array", "items": [ { - "id": "0", + "$id": "0", "enum": [ 1 ], "type": "integer" }, { - "id": "1", + "$id": "1", "enum": [ 2 ], "type": "integer" }, { - "id": "2", + "$id": "2", "enum": [ "a" ], "type": "string" }, { - "id": "3", + "$id": "3", "enum": [ "b" ], @@ -421,19 +421,19 @@ const sampleJsonSchema = ` ] }, "array_of_same_simple_types": { - "id": "array_of_same_simple_types", + "$id": "array_of_same_simple_types", "additionalItems": false, "type": "array", "items": [ { - "id": "0", + "$id": "0", "enum": [ "a" ], "type": "string" }, { - "id": "1", + "$id": "1", "enum": [ "b" ], @@ -533,7 +533,7 @@ const sampleJsonBodyTestingAmandaMessages = { const sampleJsonSchemaNonStrict = ` { "$schema": "http://json-schema.org/draft-04/schema#", - "id": "#", + "$id": "#", "required": [ "simple_key_value_pair", "complex_key_value_pair", @@ -545,29 +545,29 @@ const sampleJsonSchemaNonStrict = ` "type": "object", "properties": { "simple_key_value_pair": { - "id": "simple_key_value_pair" + "$id": "simple_key_value_pair" }, "complex_key_value_pair": { - "id": "complex_key_value_pair", + "$id": "complex_key_value_pair", "additionalProperties": true, "type": "object", "properties": { "complex_key_value_pair_key1": { - "id": "complex_key_value_pair_key1" + "$id": "complex_key_value_pair_key1" }, "complex_key_value_pair_key2": { - "id": "complex_key_value_pair_key2" + "$id": "complex_key_value_pair_key2" }, "complex_key_value_pair_key3": { - "id": "complex_key_value_pair_key3", + "$id": "complex_key_value_pair_key3", "additionalProperties": true, "type": "object", "properties": { "complex_key_value_pair_key1_in_nested_hash": { - "id": "complex_key_value_pair_key1_in_nested_hash" + "$id": "complex_key_value_pair_key1_in_nested_hash" }, "complex_key_value_pair_key2_in_nested_hash": { - "id": "complex_key_value_pair_key2_in_nested_hash" + "$id": "complex_key_value_pair_key2_in_nested_hash" } }, "required": [ @@ -583,12 +583,12 @@ const sampleJsonSchemaNonStrict = ` ] }, "array_of_hashes": { - "id": "array_of_hashes", + "$id": "array_of_hashes", "additionalItems": true, "type": "array", "items": [ { - "id": "0", + "$id": "0", "required": [ "array_of_hashes_item1_key1", "array_of_hashes_item1_key2" @@ -597,15 +597,15 @@ const sampleJsonSchemaNonStrict = ` "type": "object", "properties": { "array_of_hashes_item1_key1": { - "id": "array_of_hashes_item1_key1" + "$id": "array_of_hashes_item1_key1" }, "array_of_hashes_item1_key2": { - "id": "array_of_hashes_item1_key2" + "$id": "array_of_hashes_item1_key2" } } }, { - "id": "1", + "$id": "1", "required": [ "array_of_hashes_item2_key1", "array_of_hashes_item2_key2" @@ -614,44 +614,44 @@ const sampleJsonSchemaNonStrict = ` "type": "object", "properties": { "array_of_hashes_item2_key1": { - "id": "array_of_hashes_item2_key1" + "$id": "array_of_hashes_item2_key1" }, "array_of_hashes_item2_key2": { - "id": "array_of_hashes_item2_key2" + "$id": "array_of_hashes_item2_key2" } } } ] }, "array_of_mixed_simple_types": { - "id": "array_of_mixed_simple_types", + "$id": "array_of_mixed_simple_types", "additionalItems": true, "type": "array", "items": [ { - "id": "0" + "$id": "0" }, { - "id": "1" + "$id": "1" }, { - "id": "2" + "$id": "2" }, { - "id": "3" + "$id": "3" } ] }, "array_of_same_simple_types": { - "id": "array_of_same_simple_types", + "$id": "array_of_same_simple_types", "additionalItems": true, "type": "array", "items": [ { - "id": "0" + "$id": "0" }, { - "id": "1" + "$id": "1" } ] } diff --git a/test/fixtures/valid-schema-v4.json b/test/fixtures/valid-schema-v4.json index baf6f059..ea873a14 100644 --- a/test/fixtures/valid-schema-v4.json +++ b/test/fixtures/valid-schema-v4.json @@ -8,7 +8,6 @@ "type": "number" }, "bar": { - "type": "string", "enum": ["a", "b", "c"] } } diff --git a/test/regression/all-of-additional-properties.test.js b/test/regression/all-of-additional-properties.test.js new file mode 100644 index 00000000..1d82ff9d --- /dev/null +++ b/test/regression/all-of-additional-properties.test.js @@ -0,0 +1,149 @@ +/** + * Previously validation of JSON Schema Draft 3 schemas ignored + * the ".allOf" property, which resulted into unexpected validation result. + * @see https://github.com/apiaryio/dredd/issues/1647 + */ +const { expect } = require('../chai'); +const gavel = require('../../lib'); + +const schema = { + $schema: 'http://json-schema.org/draft-04/schema#', + allOf: [ + { + type: 'object', + required: ['id', 'body'], + properties: { + id: { + type: 'string', + format: 'uuid' + } + } + }, + { + additionalProperties: false, + properties: { + id: { + type: 'string', + format: 'uuid' + }, + body: { + type: 'string' + }, + additional_details: { + type: 'object', + 'x-nullable': true + }, + additional_equipment: { + type: 'array', + 'x-nullable': true + } + } + } + ] +}; + +describe('JSON Schema with "additionalProperties" and "allOf"', () => { + describe('given a detached JSON Schema with "allOf"', () => { + describe('and the actual body matches the schema', () => { + let result; + + before(() => { + result = gavel.validate( + { + bodySchema: schema + }, + { + body: JSON.stringify({ + id: '123e4567-e89b-12d3-a456-426655440000', + body: 'any' + }) + } + ); + }); + + it('should be valid', () => { + expect(result).to.be.valid; + }); + + it('should not have any errors', () => { + expect(result.fields.body).to.not.have.errors; + }); + }); + + describe('and the actual body does not match the schema', () => { + let result; + + before(() => { + result = gavel.validate( + { + bodySchema: schema + }, + { + body: JSON.stringify({ + id: 2 // invalid type + }) + } + ); + }); + + it('should not be valid', () => { + expect(result).not.to.be.valid; + }); + + it('should have exactly 3 errors', () => { + expect(result.fields.body).to.have.errors.lengthOf(3); + }); + + describe('given missing property error', () => { + it('should have an error message about a missing "body" property', () => { + expect(result.fields.body) + .to.have.errorAtIndex(0) + .withMessage(`At '/body' Missing required property: body`); + }); + + it('should have a verbose error location', () => { + expect(result.fields.body) + .to.have.errorAtIndex(0) + .withLocation({ + pointer: '/body', + property: ['body'] + }); + }); + }); + + describe('given invalid type error', () => { + it('should have an error message about invalid "id" type', () => { + expect(result.fields.body) + .to.have.errorAtIndex(1) + .withMessage(`At '/id' Invalid type: number (expected string)`); + }); + + it('should have a verbose error location', () => { + expect(result.fields.body) + .to.have.errorAtIndex(1) + .withLocation({ + pointer: '/id', + property: ['id'] + }); + }); + }); + + describe('given another invalid type error', () => { + it('should have an error message about invalid "id" type', () => { + expect(result.fields.body) + .to.have.errorAtIndex(2) + .withMessage(`At '/id' Invalid type: number (expected string)`); + }); + + it('should have a verbose error location', () => { + expect(result.fields.body) + .to.have.errorAtIndex(2) + .withLocation({ + pointer: '/id', + property: ['id'] + }); + }); + }); + }); + }); +}); diff --git a/test/regression/json-root-array.test.js b/test/regression/json-root-array.test.js index c97f2b63..e68411dd 100644 --- a/test/regression/json-root-array.test.js +++ b/test/regression/json-root-array.test.js @@ -61,7 +61,7 @@ describe('Root-level array validation', () => { it('should have explanatory message', () => { expect(result.fields.body) .to.have.errorAtIndex(0) - .withMessage('data/1 should be string'); + .withMessage(`At '/1' Invalid type: number (expected string)`); }); describe('should have "location"', () => { diff --git a/test/unit/units/validateBody/getBodyValidator.test.js b/test/unit/units/validateBody/getBodyValidator.test.js index 29d47eb1..34046b9b 100644 --- a/test/unit/units/validateBody/getBodyValidator.test.js +++ b/test/unit/units/validateBody/getBodyValidator.test.js @@ -15,7 +15,7 @@ describe('getBodyValidator', () => { }, { contentTypes: ['application/schema+json', 'application/json'], - expectedValidator: 'JsonSchema' + expectedValidator: 'JsonSchemaValidator' }, { contentTypes: ['text/plain', 'text/plain'], diff --git a/test/unit/validators/json-schema-legacy.test.js b/test/unit/validators/json-schema-legacy.test.js deleted file mode 100644 index a5212947..00000000 --- a/test/unit/validators/json-schema-legacy.test.js +++ /dev/null @@ -1,120 +0,0 @@ -const { expect } = require('chai'); -const { - JsonSchemaLegacy -} = require('../../../lib/validators/json-schema-legacy'); - -const errorTypes = require('../../../lib/errors'); -const invalidJsonSchema4 = require('../../fixtures/invalid-schema-v4'); -const validJsonSchema4 = require('../../fixtures/valid-schema-v4'); - -describe('JSON Schema (legacy)', () => { - /** - * V4 - */ - describe('given JSON Schema Draft 4', () => { - describe('and the schema is invalid', () => { - let init; - - before(() => { - init = () => new JsonSchemaLegacy(invalidJsonSchema4); - }); - - it('should throw an error about the invalid schema', () => { - expect(init).to.throw(errorTypes.JsonSchemaNotValid); - }); - }); - - describe('and the schema is valid', () => { - let init; - let validator; - - before(() => { - init = () => { - validator = new JsonSchemaLegacy(validJsonSchema4); - }; - }); - - it('should not throw any errors', () => { - expect(init).not.to.throw(); - }); - - it('should recognize schema version as v4', () => { - expect(validator).to.have.property('jsonSchemaVersion', 'draftV4'); - }); - - describe('given validating data', () => { - describe('and the data is invalid', () => { - let errors; - const data = { - foo: 'should be number', - bar: 'z' - }; - - before(() => { - errors = validator.validate(data); - }); - - describe('should return validation errors', () => { - it('should have 2 error', () => { - expect(errors).to.have.lengthOf(2); - }); - - describe('first error', () => { - it('should have an error message', () => { - expect(errors[0].message).to.equal( - `At '/foo' Invalid type: string (expected number)` - ); - }); - - it('should have a pointer', () => { - expect(errors[0]).to.have.nested.property( - 'location.pointer', - '/foo' - ); - }); - - it('should have a property name', () => { - expect(errors[0].location.property).to.deep.equal(['foo']); - }); - }); - - describe('second error', () => { - it('should have an error message', () => { - expect(errors[1].message).to.equal( - `At '/bar' No enum match for: "z"` - ); - }); - - it('should have a pointer', () => { - expect(errors[1]).to.have.nested.property( - 'location.pointer', - '/bar' - ); - }); - - it('should have a property name', () => { - expect(errors[1].location.property).to.deep.equal(['bar']); - }); - }); - }); - }); - - describe('and the data is valid', () => { - let errors; - const data = { - foo: 123, - bar: 'a' - }; - - before(() => { - errors = validator.validate(data); - }); - - it('should return no errors', () => { - expect(errors).to.have.lengthOf(0); - }); - }); - }); - }); - }); -}); diff --git a/test/unit/validators/json-schema-next.test.js b/test/unit/validators/json-schema-next.test.js index 5521f07e..3928dc36 100644 --- a/test/unit/validators/json-schema-next.test.js +++ b/test/unit/validators/json-schema-next.test.js @@ -4,6 +4,8 @@ const { } = require('../../../lib/validators/json-schema-next'); const errorTypes = require('../../../lib/errors'); +const invalidJsonSchema4 = require('../../fixtures/invalid-schema-v4'); +const validJsonSchema4 = require('../../fixtures/valid-schema-v4'); const invalidJsonSchema6 = require('../../fixtures/invalid-schema-v6'); const validJsonSchema6 = require('../../fixtures/valid-schema-v6'); const invalidJsonSchema7 = require('../../fixtures/invalid-schema-v7'); @@ -32,6 +34,116 @@ describe('JSON Schema (next)', () => { }); }); + /** + * Draft 4 + */ + describe('given JSON Schema Draft 4', () => { + describe('and the schema is invalid', () => { + let init; + + before(() => { + init = () => new JsonSchemaValidator(invalidJsonSchema4); + }); + + it('should throw an error about the invalid schema', () => { + expect(init).to.throw(errorTypes.JsonSchemaNotValid); + }); + }); + + describe('and the schema is valid', () => { + let init; + let validator; + + before(() => { + init = () => { + validator = new JsonSchemaValidator(validJsonSchema4); + }; + }); + + it('should not throw any errors', () => { + expect(init).not.to.throw(); + }); + + it('should recognize schema version as v4', () => { + expect(validator).to.have.property('jsonSchemaVersion', 'draftV4'); + }); + + describe('given validating data', () => { + describe('and the data is invalid', () => { + let errors; + const data = { + foo: 'should be number', + bar: 'z' + }; + + before(() => { + errors = validator.validate(data); + }); + + describe('should return validation errors', () => { + it('should have 2 errors', () => { + expect(errors).to.have.lengthOf(2); + }); + + describe('first error', () => { + it('should have an error message', () => { + expect(errors[0].message).to.equal( + `At '/foo' Invalid type: string (expected number)` + ); + }); + + it('should have a pointer', () => { + expect(errors[0]).to.have.nested.property( + 'location.pointer', + '/foo' + ); + }); + + it('should have a property name', () => { + expect(errors[0].location.property).to.deep.equal(['foo']); + }); + }); + + describe('second error', () => { + it('should have an error message', () => { + expect(errors[1].message).to.equal( + `At '/bar' No enum match for: "z"` + ); + }); + + it('should have a pointer', () => { + expect(errors[1]).to.have.nested.property( + 'location.pointer', + '/bar' + ); + }); + + it('should have a property name', () => { + expect(errors[1].location.property).to.deep.equal(['bar']); + }); + }); + }); + }); + + describe('and the data is valid', () => { + let errors; + const data = { + foo: 123, + bar: 'a' + }; + + before(() => { + errors = validator.validate(data); + }); + + it('should return no errors', () => { + expect(errors).to.have.lengthOf(0); + }); + }); + }); + }); + }); + /** * Draft 6 */ @@ -100,7 +212,7 @@ describe('JSON Schema (next)', () => { it('should contain the error message', () => { expect(errors[0]).to.have.property( 'message', - 'data/foo should be number' + `At '/foo' Invalid type: string (expected number)` ); }); diff --git a/test/unit/validators/json-schema.test.js b/test/unit/validators/json-schema.test.js index 2a5e81aa..92c93be3 100644 --- a/test/unit/validators/json-schema.test.js +++ b/test/unit/validators/json-schema.test.js @@ -1,7 +1,9 @@ /* eslint-disable */ const { assert } = require('chai'); const fixtures = require('../../fixtures'); -const { JsonSchema } = require('../../../lib/validators/json-schema'); +const { + JsonSchemaValidator +} = require('../../../lib/validators/json-schema-next'); const { ValidationErrors } = require('../../../lib/validators/validation-errors'); @@ -28,11 +30,11 @@ describe('JsonSchema', () => { let validator = null; beforeEach(() => { - validator = new JsonSchema(data.schema); + validator = new JsonSchemaValidator(data.schema); }); it('should not throw an exception', () => { - const fn = () => new JsonSchema(data.schema); + const fn = () => new JsonSchemaValidator(data.schema); assert.doesNotThrow(fn); }); @@ -108,12 +110,12 @@ describe('JsonSchema', () => { }); }); - // shared.shouldBehaveLikeAmandaToGavel(new JsonSchema('{}')); + // shared.shouldBehaveLikeAmandaToGavel(new JsonSchemaValidator('{}')); }); describe('when validation performed on actual empty object', () => { it('should return some errors', () => { - validator = new JsonSchema( + validator = new JsonSchemaValidator( JSON.parse(fixtures.sampleJsonSchemaNonStrict) ); errors = validator.validate({}); @@ -125,7 +127,7 @@ describe('JsonSchema', () => { * @deprecate Stop testing implementation detail. */ it('should have validateSchema method', () => { - validator = new JsonSchema({}); + validator = new JsonSchemaValidator({}); assert.isDefined(validator.validateSchema); }); @@ -134,7 +136,7 @@ describe('JsonSchema', () => { it('should throw an error for "data"', () => { const fn = () => { - new JsonSchema(invalidStringifiedSchema); + new JsonSchemaValidator(invalidStringifiedSchema); }; assert.throw(fn); }); @@ -142,7 +144,7 @@ describe('JsonSchema', () => { it('should throw an error for "schema"', () => { const invalidStringifiedSchema = require('../../fixtures/invalid-stringified-schema'); const fn = () => { - new JsonSchema(invalidStringifiedSchema); + new JsonSchemaValidator(invalidStringifiedSchema); }; assert.throw(fn); }); @@ -155,7 +157,7 @@ describe('JsonSchema', () => { before(() => { const invalidSchema = require('../../fixtures/invalid-schema-v4'); fn = () => { - validator = new JsonSchema(invalidSchema); + validator = new JsonSchemaValidator(invalidSchema); }; }); @@ -177,7 +179,7 @@ describe('JsonSchema', () => { before(() => { validSchema = require('../../fixtures/valid-schema-v4'); fn = () => { - validator = new JsonSchema(validSchema); + validator = new JsonSchemaValidator(validSchema); }; }); @@ -198,7 +200,7 @@ describe('JsonSchema', () => { validSchema = require('../../fixtures/valid-schema-v4'); delete validSchema.$schema; fn = () => { - validator = new JsonSchema(validSchema); + validator = new JsonSchemaValidator(validSchema); }; }); @@ -218,7 +220,7 @@ describe('JsonSchema', () => { validSchema = require('../../fixtures/invalid-schema-v3-v4'); delete validSchema.$schema; fn = () => { - validator = new JsonSchema(validSchema); + validator = new JsonSchemaValidator(validSchema); }; }); diff --git a/typings.d.ts b/typings.d.ts index c002860e..ac97f915 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -15,7 +15,7 @@ declare module 'gavel' { headers: Record | string; body: Record | string; /** - * [JSON Schema](https://json-schema.org/) Draft V3-4. + * Instance of the supported version of [JSON Schema](https://json-schema.org/). */ bodySchema: Record | string; } @@ -33,7 +33,7 @@ declare module 'gavel' { valid: boolean; /** * Validation results of each individual HTTP message - * field (i.e. "statusCode", "body", etc). + * field (i.e. `statusCode`, `body`, etc). */ fields: Record; } @@ -54,6 +54,9 @@ declare module 'gavel' { expected: any; actual: any; }; + /** + * The list of validation errors, if any. + */ errors: FieldValidationError[]; } @@ -64,8 +67,17 @@ declare module 'gavel' { * Dependends on the HTTP message field's "kind" property. */ location?: { + /** + * A complete JSON pointer to the related property in the data. + */ pointer?: string; property?: string[]; + /** + * A JSON pointer to the relevant rule in the JSON Schema. + * Applicable only if validating using `bodySchema` in + * the expected HTTP message. + */ + schemaPath?: string; }; }