From 33d549d493f595ab6362dd89dc5652e4583d5897 Mon Sep 17 00:00:00 2001 From: Erayd Date: Thu, 23 Feb 2017 02:26:21 +1300 Subject: [PATCH] Add option to also validate the schema --- README.md | 1 + schema-validation/json-schema-draft-03.json | 193 +++++++++++++++ schema-validation/json-schema-draft-04.json | 221 ++++++++++++++++++ src/JsonSchema/ConstraintError.php | 2 + src/JsonSchema/Constraints/BaseConstraint.php | 48 +++- src/JsonSchema/Constraints/Constraint.php | 1 + src/JsonSchema/Constraints/Factory.php | 26 +++ .../Constraints/SchemaConstraint.php | 57 ++++- .../Exception/InvalidSchemaException.php | 17 ++ src/JsonSchema/Validator.php | 7 + .../Constraints/AdditionalPropertiesTest.php | 7 +- tests/Constraints/ArraysTest.php | 2 + tests/Constraints/BaseTestCase.php | 47 +++- tests/Constraints/BasicTypesTest.php | 3 + tests/Constraints/CoerciveTest.php | 3 + tests/Constraints/DependenciesTest.php | 3 + tests/Constraints/DisallowTest.php | 8 + tests/Constraints/DivisibleByTest.php | 2 + tests/Constraints/EnumTest.php | 3 + tests/Constraints/ExtendsTest.php | 3 + tests/Constraints/FormatTest.php | 2 + tests/Constraints/LongArraysTest.php | 2 + tests/Constraints/MinItemsMaxItemsTest.php | 2 + .../MinLengthMaxLengthMultiByteTest.php | 2 + tests/Constraints/MinLengthMaxLengthTest.php | 2 + tests/Constraints/MinMaxPropertiesTest.php | 2 + tests/Constraints/MinimumMaximumTest.php | 2 + tests/Constraints/NotTest.php | 2 + .../Constraints/NumberAndIntegerTypesTest.php | 2 + tests/Constraints/OfPropertiesTest.php | 13 +- tests/Constraints/PatternPropertiesTest.php | 2 + tests/Constraints/PatternTest.php | 2 + tests/Constraints/PointerTest.php | 14 +- tests/Constraints/ReadOnlyTest.php | 2 + tests/Constraints/RequireTest.php | 2 + tests/Constraints/RequiredPropertyTest.php | 6 + tests/Constraints/SchemaValidationTest.php | 124 ++++++++++ tests/Constraints/SelfDefinedSchemaTest.php | 4 + tests/Constraints/TupleTypingTest.php | 2 + tests/Constraints/UnionTypesTest.php | 2 + tests/Constraints/UnionWithNullValueTest.php | 2 + tests/Constraints/UniqueItemsTest.php | 2 + .../WrongMessagesFailingTestCaseTest.php | 2 + tests/Drafts/Draft3Test.php | 3 + tests/Drafts/Draft4Test.php | 3 + 45 files changed, 838 insertions(+), 19 deletions(-) create mode 100644 schema-validation/json-schema-draft-03.json create mode 100644 schema-validation/json-schema-draft-04.json create mode 100644 src/JsonSchema/Exception/InvalidSchemaException.php create mode 100644 tests/Constraints/SchemaValidationTest.php diff --git a/README.md b/README.md index 4de425f5..b2ddd35a 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ third argument to `Validator::validate()`, or can be provided as the third argum | `Constraint::CHECK_MODE_APPLY_DEFAULTS` | Apply default values from the schema if not set | | `Constraint::CHECK_MODE_EXCEPTIONS` | Throw an exception immediately if validation fails | | `Constraint::CHECK_MODE_DISABLE_FORMAT` | Do not validate "format" constraints | +| `Constraint::CHECK_MODE_VALIDATE_SCHEMA` | Validate the schema as well as the provided document | Please note that using `Constraint::CHECK_MODE_COERCE_TYPES` or `Constraint::CHECK_MODE_APPLY_DEFAULTS` will modify your original data. diff --git a/schema-validation/json-schema-draft-03.json b/schema-validation/json-schema-draft-03.json new file mode 100644 index 00000000..dcf07342 --- /dev/null +++ b/schema-validation/json-schema-draft-03.json @@ -0,0 +1,193 @@ +{ + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "http://json-schema.org/draft-03/schema#", + "type": "object", + "properties": { + "type": { + "type": [ + "string", + "array" + ], + "items": { + "type": [ + "string", + { + "$ref": "#" + } + ] + }, + "uniqueItems": true, + "default": "any" + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "additionalProperties": { + "type": [ + { + "$ref": "#" + }, + "boolean" + ], + "default": {} + }, + "items": { + "type": [ + { + "$ref": "#" + }, + "array" + ], + "items": { + "$ref": "#" + }, + "default": {} + }, + "additionalItems": { + "type": [ + { + "$ref": "#" + }, + "boolean" + ], + "default": {} + }, + "required": { + "type": "boolean", + "default": false + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "type": [ + "string", + "array", + { + "$ref": "#" + } + ], + "items": { + "type": "string" + } + }, + "default": {} + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minItems": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "minLength": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "maxLength": { + "type": "integer" + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "default": { + "type": "any" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "format": { + "type": "string" + }, + "divisibleBy": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 1 + }, + "disallow": { + "type": [ + "string", + "array" + ], + "items": { + "type": [ + "string", + { + "$ref": "#" + } + ] + }, + "uniqueItems": true + }, + "extends": { + "type": [ + { + "$ref": "#" + }, + "array" + ], + "items": { + "$ref": "#" + }, + "default": {} + }, + "id": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri" + }, + "$schema": { + "type": "string", + "format": "uri" + } + }, + "dependencies": { + "exclusiveMinimum": "minimum", + "exclusiveMaximum": "maximum" + }, + "default": {} +} \ No newline at end of file diff --git a/schema-validation/json-schema-draft-04.json b/schema-validation/json-schema-draft-04.json new file mode 100644 index 00000000..d13c1cf2 --- /dev/null +++ b/schema-validation/json-schema-draft-04.json @@ -0,0 +1,221 @@ +{ + "id": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#" + } + }, + "positiveInteger": { + "type": "integer", + "minimum": 0 + }, + "positiveIntegerDefault0": { + "allOf": [ + { + "$ref": "#/definitions/positiveInteger" + }, + { + "default": 0 + } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uri" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { + "$ref": "#/definitions/positiveInteger" + }, + "minLength": { + "$ref": "#/definitions/positiveIntegerDefault0" + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#" + } + ], + "default": {} + }, + "items": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/schemaArray" + } + ], + "default": {} + }, + "maxItems": { + "$ref": "#/definitions/positiveInteger" + }, + "minItems": { + "$ref": "#/definitions/positiveIntegerDefault0" + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { + "$ref": "#/definitions/positiveInteger" + }, + "minProperties": { + "$ref": "#/definitions/positiveIntegerDefault0" + }, + "required": { + "$ref": "#/definitions/stringArray" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#" + } + ], + "default": {} + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/stringArray" + } + ] + } + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { + "$ref": "#/definitions/simpleTypes" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/simpleTypes" + }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "allOf": { + "$ref": "#/definitions/schemaArray" + }, + "anyOf": { + "$ref": "#/definitions/schemaArray" + }, + "oneOf": { + "$ref": "#/definitions/schemaArray" + }, + "not": { + "$ref": "#" + } + }, + "dependencies": { + "exclusiveMaximum": [ + "maximum" + ], + "exclusiveMinimum": [ + "minimum" + ] + }, + "default": {} +} diff --git a/src/JsonSchema/ConstraintError.php b/src/JsonSchema/ConstraintError.php index 2af542d8..9ea02046 100644 --- a/src/JsonSchema/ConstraintError.php +++ b/src/JsonSchema/ConstraintError.php @@ -28,6 +28,7 @@ class ConstraintError extends \MabeEnum\Enum const FORMAT_STYLE = 'styleFormat'; const FORMAT_TIME = 'timeFormat'; const FORMAT_URL = 'urlFormat'; + const INVALID_SCHEMA = 'invalidSchema'; const LENGTH_MAX = 'maxLength'; const LENGTH_MIN = 'minLength'; const MAXIMUM = 'maximum'; @@ -77,6 +78,7 @@ public function getMessage() self::FORMAT_TIME => 'Invalid time %s, expected format hh:mm:ss', self::FORMAT_URL => 'Invalid URL format', self::LENGTH_MAX => 'Must be at most %d characters long', + self::INVALID_SCHEMA => 'Schema is not valid', self::LENGTH_MIN => 'Must be at least %d characters long', self::MAX_ITEMS => 'There must be a maximum of %d items in the array', self::MAXIMUM => 'Must have a maximum value less than or equal to %d', diff --git a/src/JsonSchema/Constraints/BaseConstraint.php b/src/JsonSchema/Constraints/BaseConstraint.php index bc608f6d..61f23edd 100644 --- a/src/JsonSchema/Constraints/BaseConstraint.php +++ b/src/JsonSchema/Constraints/BaseConstraint.php @@ -12,6 +12,7 @@ use JsonSchema\ConstraintError; use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\ValidationException; +use JsonSchema\Validator; /** * A more basic constraint definition - used for the public @@ -24,6 +25,11 @@ class BaseConstraint */ protected $errors = array(); + /** + * @var int All error types which have occurred + */ + protected $errorMask = Validator::ERROR_NONE; + /** * @var Factory */ @@ -54,7 +60,8 @@ public function addError(ConstraintError $constraint, JsonPointer $path = null, 'constraint' => array( 'name' => $name, 'params' => $more - ) + ), + 'context' => $this->factory->getErrorContext(), ); if ($this->factory->getConfig(Constraint::CHECK_MODE_EXCEPTIONS)) { @@ -62,18 +69,42 @@ public function addError(ConstraintError $constraint, JsonPointer $path = null, } $this->errors[] = $error; + $this->errorMask |= $error['context']; } public function addErrors(array $errors) { if ($errors) { $this->errors = array_merge($this->errors, $errors); + $errorMask = &$this->errorMask; + array_walk($errors, function ($error) use (&$errorMask) { + if (isset($error['context'])) { + $errorMask |= $error['context']; + } + }); + } + } + + public function getErrors($errorContext = Validator::ERROR_ALL) + { + if ($errorContext === Validator::ERROR_ALL) { + return $this->errors; } + + return array_filter($this->errors, function ($error) use ($errorContext) { + if ($errorContext & $error['context']) { + return true; + } + }); } - public function getErrors() + public function numErrors($errorContext = Validator::ERROR_ALL) { - return $this->errors; + if ($errorContext === Validator::ERROR_ALL) { + return count($this->errors); + } + + return count($this->getErrors($errorContext)); } public function isValid() @@ -88,5 +119,16 @@ public function isValid() public function reset() { $this->errors = array(); + $this->errorMask = Validator::ERROR_NONE; + } + + /** + * Get the error mask + * + * @return int + */ + public function getErrorMask() + { + return $this->errorMask; } } diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index 2283ac12..ce0e2be5 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -31,6 +31,7 @@ abstract class Constraint extends BaseConstraint implements ConstraintInterface const CHECK_MODE_APPLY_DEFAULTS = 0x00000008; const CHECK_MODE_EXCEPTIONS = 0x00000010; const CHECK_MODE_DISABLE_FORMAT = 0x00000020; + const CHECK_MODE_VALIDATE_SCHEMA = 0x00000100; /** * Bubble down the path diff --git a/src/JsonSchema/Constraints/Factory.php b/src/JsonSchema/Constraints/Factory.php index 8c24873f..c9296385 100644 --- a/src/JsonSchema/Constraints/Factory.php +++ b/src/JsonSchema/Constraints/Factory.php @@ -16,6 +16,7 @@ use JsonSchema\SchemaStorageInterface; use JsonSchema\Uri\UriRetriever; use JsonSchema\UriRetrieverInterface; +use JsonSchema\Validator; /** * Factory for centralize constraint initialization. @@ -42,6 +43,11 @@ class Factory */ private $typeCheck = array(); + /** + * @var int Validation context + */ + protected $errorContext = Validator::ERROR_DOCUMENT_VALIDATION; + /** * @var array */ @@ -193,4 +199,24 @@ public function createInstanceFor($constraintName) return clone $this->instanceCache[$constraintName]; } + + /** + * Get the error context + * + * @return string + */ + public function getErrorContext() + { + return $this->errorContext; + } + + /** + * Set the error context + * + * @param string $validationContext + */ + public function setErrorContext($errorContext) + { + $this->errorContext = $errorContext; + } } diff --git a/src/JsonSchema/Constraints/SchemaConstraint.php b/src/JsonSchema/Constraints/SchemaConstraint.php index c33fe8ca..fad577b7 100644 --- a/src/JsonSchema/Constraints/SchemaConstraint.php +++ b/src/JsonSchema/Constraints/SchemaConstraint.php @@ -9,8 +9,13 @@ namespace JsonSchema\Constraints; +use JsonSchema\ConstraintError; use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\InvalidArgumentException; +use JsonSchema\Exception\InvalidSchemaException; +use JsonSchema\Exception\RuntimeException; +use JsonSchema\SchemaStorage; +use JsonSchema\Validator; /** * The SchemaConstraint Constraints, validates an element against a given schema @@ -20,6 +25,8 @@ */ class SchemaConstraint extends Constraint { + const DEFAULT_SCHEMA_SPEC = 'http://json-schema.org/draft-04/schema#'; + /** * {@inheritdoc} */ @@ -27,16 +34,62 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i = { if ($schema !== null) { // passed schema - $this->checkUndefined($element, $schema, $path, $i); + $validationSchema = $schema; } elseif ($this->getTypeCheck()->propertyExists($element, $this->inlineSchemaProperty)) { $inlineSchema = $this->getTypeCheck()->propertyGet($element, $this->inlineSchemaProperty); if (is_array($inlineSchema)) { $inlineSchema = json_decode(json_encode($inlineSchema)); } // inline schema - $this->checkUndefined($element, $inlineSchema, $path, $i); + $validationSchema = $inlineSchema; } else { throw new InvalidArgumentException('no schema found to verify against'); } + + // validate schema against whatever is defined in $validationSchema->$schema. If no + // schema is defined, assume self::DEFAULT_SCHEMA_SPEC (currently draft-04). + if ($this->factory->getConfig(self::CHECK_MODE_VALIDATE_SCHEMA)) { + if (!$this->getTypeCheck()->isObject($validationSchema)) { + throw new RuntimeException('Cannot validate the schema of a non-object'); + } + if ($this->getTypeCheck()->propertyExists($validationSchema, '$schema')) { + $schemaSpec = $this->getTypeCheck()->propertyGet($validationSchema, '$schema'); + } else { + $schemaSpec = self::DEFAULT_SCHEMA_SPEC; + } + + // get the spec schema + $schemaStorage = $this->factory->getSchemaStorage(); + if (!$this->getTypeCheck()->isObject($schemaSpec)) { + $schemaSpec = $schemaStorage->getSchema($schemaSpec); + } + + // save error count, config & subtract CHECK_MODE_VALIDATE_SCHEMA + $initialErrorCount = $this->numErrors(); + $initialConfig = $this->factory->getConfig(); + $initialContext = $this->factory->getErrorContext(); + $this->factory->removeConfig(self::CHECK_MODE_VALIDATE_SCHEMA | self::CHECK_MODE_APPLY_DEFAULTS); + $this->factory->addConfig(self::CHECK_MODE_TYPE_CAST); + $this->factory->setErrorContext(Validator::ERROR_SCHEMA_VALIDATION); + + // validate schema + try { + $this->check($validationSchema, $schemaSpec); + } catch (\Exception $e) { + if ($this->factory->getConfig(self::CHECK_MODE_EXCEPTIONS)) { + throw new InvalidSchemaException('Schema did not pass validation', 0, $e); + } + } + if ($this->numErrors() > $initialErrorCount) { + $this->addError(ConstraintError::INVALID_SCHEMA(), $path); + } + + // restore the initial config + $this->factory->setConfig($initialConfig); + $this->factory->setErrorContext($initialContext); + } + + // validate element against $validationSchema + $this->checkUndefined($element, $validationSchema, $path, $i); } } diff --git a/src/JsonSchema/Exception/InvalidSchemaException.php b/src/JsonSchema/Exception/InvalidSchemaException.php new file mode 100644 index 00000000..c1209958 --- /dev/null +++ b/src/JsonSchema/Exception/InvalidSchemaException.php @@ -0,0 +1,17 @@ +factory->setConfig($initialCheckMode); $this->addErrors(array_unique($validator->getErrors(), SORT_REGULAR)); + + return $validator->getErrorMask(); } /** diff --git a/tests/Constraints/AdditionalPropertiesTest.php b/tests/Constraints/AdditionalPropertiesTest.php index 5ecfa0a7..e064312d 100644 --- a/tests/Constraints/AdditionalPropertiesTest.php +++ b/tests/Constraints/AdditionalPropertiesTest.php @@ -9,8 +9,12 @@ namespace JsonSchema\Tests\Constraints; +use JsonSchema\Validator; + class AdditionalPropertiesTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( @@ -41,7 +45,8 @@ public function getInvalidTests() 'params' => array( 'property' => 'additionalProp' ) - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ) ) ), diff --git a/tests/Constraints/ArraysTest.php b/tests/Constraints/ArraysTest.php index 5498e35e..dac14358 100644 --- a/tests/Constraints/ArraysTest.php +++ b/tests/Constraints/ArraysTest.php @@ -11,6 +11,8 @@ class ArraysTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/BaseTestCase.php b/tests/Constraints/BaseTestCase.php index 75010e33..50efbd82 100644 --- a/tests/Constraints/BaseTestCase.php +++ b/tests/Constraints/BaseTestCase.php @@ -20,19 +20,31 @@ */ abstract class BaseTestCase extends VeryBaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-04/schema#'; + protected $validateSchema = false; + /** * @dataProvider getInvalidTests */ public function testInvalidCases($input, $schema, $checkMode = Constraint::CHECK_MODE_NORMAL, $errors = array()) { $checkMode = $checkMode === null ? Constraint::CHECK_MODE_NORMAL : $checkMode; + if ($this->validateSchema) { + $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; + } $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + if (is_object($schema) && !isset($schema->{'$schema'})) { + $schema->{'$schema'} = $this->schemaSpec; + } $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); $checkValue = json_decode($input); - $validator->validate($checkValue, $schema); + $errorMask = $validator->validate($checkValue, $schema); + + $this->assertTrue((bool) ($errorMask & Validator::ERROR_DOCUMENT_VALIDATION)); + $this->assertGreaterThan(0, $validator->numErrors()); if (array() !== $errors) { $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(), true)); @@ -46,16 +58,25 @@ public function testInvalidCases($input, $schema, $checkMode = Constraint::CHECK public function testInvalidCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_TYPE_CAST, $errors = array()) { $checkMode = $checkMode === null ? Constraint::CHECK_MODE_TYPE_CAST : $checkMode; - if ($checkMode !== Constraint::CHECK_MODE_TYPE_CAST) { + if ($this->validateSchema) { + $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; + } + if (!($checkMode & Constraint::CHECK_MODE_TYPE_CAST)) { $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_TYPE_CAST"'); } $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + if (is_object($schema) && !isset($schema->{'$schema'})) { + $schema->{'$schema'} = $this->schemaSpec; + } $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); $checkValue = json_decode($input, true); - $validator->validate($checkValue, $schema); + $errorMask = $validator->validate($checkValue, $schema); + + $this->assertTrue((bool) ($errorMask & Validator::ERROR_DOCUMENT_VALIDATION)); + $this->assertGreaterThan(0, $validator->numErrors()); if (array() !== $errors) { $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(), true)); @@ -68,12 +89,19 @@ public function testInvalidCasesUsingAssoc($input, $schema, $checkMode = Constra */ public function testValidCases($input, $schema, $checkMode = Constraint::CHECK_MODE_NORMAL) { + if ($this->validateSchema) { + $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; + } $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + if (is_object($schema) && !isset($schema->{'$schema'})) { + $schema->{'$schema'} = $this->schemaSpec; + } $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); $checkValue = json_decode($input); - $validator->validate($checkValue, $schema); + $errorMask = $validator->validate($checkValue, $schema); + $this->assertEquals(0, $errorMask); $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); } @@ -83,18 +111,25 @@ public function testValidCases($input, $schema, $checkMode = Constraint::CHECK_M */ public function testValidCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_TYPE_CAST) { - if ($checkMode !== Constraint::CHECK_MODE_TYPE_CAST) { + if ($this->validateSchema) { + $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; + } + if (!($checkMode & Constraint::CHECK_MODE_TYPE_CAST)) { $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_TYPE_CAST"'); } $schema = json_decode($schema); $schemaStorage = new SchemaStorage($this->getUriRetrieverMock($schema), new UriResolver()); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + if (is_object($schema) && !isset($schema->{'$schema'})) { + $schema->{'$schema'} = $this->schemaSpec; + } $value = json_decode($input, true); $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); - $validator->validate($value, $schema); + $errorMask = $validator->validate($value, $schema); + $this->assertEquals(0, $errorMask); $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); } diff --git a/tests/Constraints/BasicTypesTest.php b/tests/Constraints/BasicTypesTest.php index 7daa43ad..0e88ef42 100644 --- a/tests/Constraints/BasicTypesTest.php +++ b/tests/Constraints/BasicTypesTest.php @@ -11,6 +11,9 @@ class BasicTypesTest extends BaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/CoerciveTest.php b/tests/Constraints/CoerciveTest.php index 36c0cf6b..e4dd173d 100644 --- a/tests/Constraints/CoerciveTest.php +++ b/tests/Constraints/CoerciveTest.php @@ -17,6 +17,9 @@ class CoerciveTest extends BasicTypesTest { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + /** * @dataProvider getValidCoerceTests */ diff --git a/tests/Constraints/DependenciesTest.php b/tests/Constraints/DependenciesTest.php index 2e508218..f7f9d532 100644 --- a/tests/Constraints/DependenciesTest.php +++ b/tests/Constraints/DependenciesTest.php @@ -11,6 +11,9 @@ class DependenciesTest extends BaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/DisallowTest.php b/tests/Constraints/DisallowTest.php index 07446d9d..f00d2fab 100644 --- a/tests/Constraints/DisallowTest.php +++ b/tests/Constraints/DisallowTest.php @@ -11,6 +11,14 @@ class DisallowTest extends BaseTestCase { + // schemas in these tests look like draft-03, but the 'disallow' patterns provided are in + // violation of the spec - 'disallow' as defined in draft-03 accepts the same values as the + // 'type' option, and cannot take arbitrary patterns. The implementation in this library is + // probably deliberate, but noting that it's invalid, schema validation has been disabled + // for these tests. The 'disallow' option was removed permanently in draft-04. + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = false; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/DivisibleByTest.php b/tests/Constraints/DivisibleByTest.php index 8a010965..b88a87a1 100644 --- a/tests/Constraints/DivisibleByTest.php +++ b/tests/Constraints/DivisibleByTest.php @@ -11,6 +11,8 @@ class DivisibleByTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/EnumTest.php b/tests/Constraints/EnumTest.php index 0ca5b9e2..723321d0 100644 --- a/tests/Constraints/EnumTest.php +++ b/tests/Constraints/EnumTest.php @@ -11,6 +11,9 @@ class EnumTest extends BaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/ExtendsTest.php b/tests/Constraints/ExtendsTest.php index 289484f3..5df1fa27 100644 --- a/tests/Constraints/ExtendsTest.php +++ b/tests/Constraints/ExtendsTest.php @@ -11,6 +11,9 @@ class ExtendsTest extends BaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/FormatTest.php b/tests/Constraints/FormatTest.php index 5cca9c01..ad7075d9 100644 --- a/tests/Constraints/FormatTest.php +++ b/tests/Constraints/FormatTest.php @@ -15,6 +15,8 @@ class FormatTest extends BaseTestCase { + protected $validateSchema = true; + public function setUp() { date_default_timezone_set('UTC'); diff --git a/tests/Constraints/LongArraysTest.php b/tests/Constraints/LongArraysTest.php index 849c0371..2757b964 100644 --- a/tests/Constraints/LongArraysTest.php +++ b/tests/Constraints/LongArraysTest.php @@ -15,6 +15,8 @@ class LongArraysTest extends VeryBaseTestCase { + protected $validateSchema = true; + public function testLongStringArray() { $schema = diff --git a/tests/Constraints/MinItemsMaxItemsTest.php b/tests/Constraints/MinItemsMaxItemsTest.php index 1b477845..62fbaa9a 100644 --- a/tests/Constraints/MinItemsMaxItemsTest.php +++ b/tests/Constraints/MinItemsMaxItemsTest.php @@ -11,6 +11,8 @@ class MinItemsMaxItemsTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/MinLengthMaxLengthMultiByteTest.php b/tests/Constraints/MinLengthMaxLengthMultiByteTest.php index ab110a40..b19ec4f7 100644 --- a/tests/Constraints/MinLengthMaxLengthMultiByteTest.php +++ b/tests/Constraints/MinLengthMaxLengthMultiByteTest.php @@ -11,6 +11,8 @@ class MinLengthMaxLengthMultiByteTest extends BaseTestCase { + protected $validateSchema = true; + protected function setUp() { if (!extension_loaded('mbstring')) { diff --git a/tests/Constraints/MinLengthMaxLengthTest.php b/tests/Constraints/MinLengthMaxLengthTest.php index 0e09a7a3..8dfa7158 100644 --- a/tests/Constraints/MinLengthMaxLengthTest.php +++ b/tests/Constraints/MinLengthMaxLengthTest.php @@ -11,6 +11,8 @@ class MinLengthMaxLengthTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/MinMaxPropertiesTest.php b/tests/Constraints/MinMaxPropertiesTest.php index 8c3a641d..e4fd5a1a 100644 --- a/tests/Constraints/MinMaxPropertiesTest.php +++ b/tests/Constraints/MinMaxPropertiesTest.php @@ -11,6 +11,8 @@ class MinMaxPropertiesTest extends BaseTestCase { + protected $validateSchema = true; + /** * {@inheritdoc} */ diff --git a/tests/Constraints/MinimumMaximumTest.php b/tests/Constraints/MinimumMaximumTest.php index c25a7c29..508c0253 100644 --- a/tests/Constraints/MinimumMaximumTest.php +++ b/tests/Constraints/MinimumMaximumTest.php @@ -11,6 +11,8 @@ class MinimumMaximumTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/NotTest.php b/tests/Constraints/NotTest.php index 27f02225..3a950f57 100644 --- a/tests/Constraints/NotTest.php +++ b/tests/Constraints/NotTest.php @@ -11,6 +11,8 @@ class NotTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/NumberAndIntegerTypesTest.php b/tests/Constraints/NumberAndIntegerTypesTest.php index 91e1c7cb..6c7277b9 100644 --- a/tests/Constraints/NumberAndIntegerTypesTest.php +++ b/tests/Constraints/NumberAndIntegerTypesTest.php @@ -11,6 +11,8 @@ class NumberAndIntegerTypesTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/OfPropertiesTest.php b/tests/Constraints/OfPropertiesTest.php index 721195f5..a9eb0f1a 100644 --- a/tests/Constraints/OfPropertiesTest.php +++ b/tests/Constraints/OfPropertiesTest.php @@ -8,11 +8,15 @@ namespace JsonSchema\Tests\Constraints; +use JsonSchema\Validator; + /** * Class OfPropertiesTest */ class OfPropertiesTest extends BaseTestCase { + protected $validateSchema = true; + public function getValidTests() { return array( @@ -81,7 +85,8 @@ public function getInvalidTests() 'expected' => 'array', 'found' => 'a string' ) - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop2', @@ -93,7 +98,8 @@ public function getInvalidTests() 'expected' => 'array', 'found' => 'a number' ) - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop2', @@ -102,7 +108,8 @@ public function getInvalidTests() 'constraint' => array( 'name' => 'oneOf', 'params' => array() - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), ), ), diff --git a/tests/Constraints/PatternPropertiesTest.php b/tests/Constraints/PatternPropertiesTest.php index a04e45b9..8dede058 100644 --- a/tests/Constraints/PatternPropertiesTest.php +++ b/tests/Constraints/PatternPropertiesTest.php @@ -11,6 +11,8 @@ class PatternPropertiesTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/PatternTest.php b/tests/Constraints/PatternTest.php index 0f69b9ad..c017600c 100644 --- a/tests/Constraints/PatternTest.php +++ b/tests/Constraints/PatternTest.php @@ -11,6 +11,8 @@ class PatternTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/PointerTest.php b/tests/Constraints/PointerTest.php index 95c4c7b8..87ab0136 100644 --- a/tests/Constraints/PointerTest.php +++ b/tests/Constraints/PointerTest.php @@ -13,6 +13,8 @@ class PointerTest extends \PHPUnit_Framework_TestCase { + protected $validateSchema = true; + public function testVariousPointers() { $schema = array( @@ -93,7 +95,8 @@ public function testVariousPointers() 'params' => array( 'property' => 'prop1' ) - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop2.prop2.1', @@ -104,7 +107,8 @@ public function testVariousPointers() 'params' => array( 'property' => 'prop2.1' ) - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop3.prop3/1.prop3/1.1', @@ -115,7 +119,8 @@ public function testVariousPointers() 'params' => array( 'property' => 'prop3/1.1' ) - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop4[0].prop4-child', @@ -126,7 +131,8 @@ public function testVariousPointers() 'params' => array( 'property' => 'prop4-child' ) - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ) ), $validator->getErrors() diff --git a/tests/Constraints/ReadOnlyTest.php b/tests/Constraints/ReadOnlyTest.php index 7a3e8678..23434406 100644 --- a/tests/Constraints/ReadOnlyTest.php +++ b/tests/Constraints/ReadOnlyTest.php @@ -11,6 +11,8 @@ class ReadOnlyTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { //is readonly really required? diff --git a/tests/Constraints/RequireTest.php b/tests/Constraints/RequireTest.php index c10f8a7b..efb6f63e 100644 --- a/tests/Constraints/RequireTest.php +++ b/tests/Constraints/RequireTest.php @@ -11,6 +11,8 @@ class RequireTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/RequiredPropertyTest.php b/tests/Constraints/RequiredPropertyTest.php index 10d42f57..31545a64 100644 --- a/tests/Constraints/RequiredPropertyTest.php +++ b/tests/Constraints/RequiredPropertyTest.php @@ -14,6 +14,12 @@ class RequiredPropertyTest extends BaseTestCase { + // Most tests are draft-03 compliant, but some tests are draft-04, or mix draft-03 and + // draft-04 syntax within the same schema. Unfortunately, draft-03 and draft-04 required + // definitions are incompatible, so disabling schema validation for these tests. + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = false; + public function testErrorPropertyIsPopulatedForRequiredIfMissingInInput() { $validator = new UndefinedConstraint(); diff --git a/tests/Constraints/SchemaValidationTest.php b/tests/Constraints/SchemaValidationTest.php new file mode 100644 index 00000000..356637ae --- /dev/null +++ b/tests/Constraints/SchemaValidationTest.php @@ -0,0 +1,124 @@ +validate($input, $schema, Constraint::CHECK_MODE_VALIDATE_SCHEMA); + + $this->assertTrue((bool) (Validator::ERROR_SCHEMA_VALIDATION & $errorMask)); + $this->assertGreaterThan(0, $v->numErrors(Validator::ERROR_SCHEMA_VALIDATION)); + $this->assertEquals(0, $v->numErrors(Validator::ERROR_DOCUMENT_VALIDATION)); + + $this->assertFalse($v->isValid(), 'Validation succeeded for an invalid test case'); + foreach ($v->getErrors() as $error) { + $this->assertEquals(Validator::ERROR_SCHEMA_VALIDATION, $error['context']); + } + } + + /** + * @dataProvider getValidTests + */ + public function testValidCases($schema) + { + $input = json_decode('{"propertyOne":"valueOne"}'); + $schema = json_decode($schema); + + $v = new Validator(); + $errorMask = $v->validate($input, $schema, Constraint::CHECK_MODE_VALIDATE_SCHEMA); + $this->assertEquals(0, $errorMask); + + if (!$v->isValid()) { + var_dump($v->getErrors(Validator::ERROR_SCHEMA_VALIDATION)); + } + $this->assertTrue($v->isValid(), 'Validation failed on a valid test case'); + } + + public function testNonObjectSchema() + { + $this->setExpectedException( + '\JsonSchema\Exception\RuntimeException', + 'Cannot validate the schema of a non-object' + ); + $this->testValidCases('"notAnObject"'); + } + + public function testInvalidSchemaException() + { + $this->setExpectedException( + '\JsonSchema\Exception\InvalidSchemaException', + 'Schema did not pass validation' + ); + + $input = json_decode('{}'); + $schema = json_decode('{"properties":{"propertyOne":{"type":"string","required":true}}}'); + + $v = new Validator(); + $v->validate($input, $schema, Constraint::CHECK_MODE_VALIDATE_SCHEMA | Constraint::CHECK_MODE_EXCEPTIONS); + } +} diff --git a/tests/Constraints/SelfDefinedSchemaTest.php b/tests/Constraints/SelfDefinedSchemaTest.php index d2cce50e..e7d3d70b 100644 --- a/tests/Constraints/SelfDefinedSchemaTest.php +++ b/tests/Constraints/SelfDefinedSchemaTest.php @@ -13,12 +13,15 @@ class SelfDefinedSchemaTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( array( '{ "$schema": { + "$schema": "http://json-schema.org/draft-04/schema#", "properties": { "name": { "type": "string" @@ -44,6 +47,7 @@ public function getValidTests() array( '{ "$schema": { + "$schema": "http://json-schema.org/draft-04/schema#", "properties": { "name": { "type": "string" diff --git a/tests/Constraints/TupleTypingTest.php b/tests/Constraints/TupleTypingTest.php index ceab8ec6..08fedc0a 100644 --- a/tests/Constraints/TupleTypingTest.php +++ b/tests/Constraints/TupleTypingTest.php @@ -11,6 +11,8 @@ class TupleTypingTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/UnionTypesTest.php b/tests/Constraints/UnionTypesTest.php index 01e49c4e..42676308 100644 --- a/tests/Constraints/UnionTypesTest.php +++ b/tests/Constraints/UnionTypesTest.php @@ -11,6 +11,8 @@ class UnionTypesTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/UnionWithNullValueTest.php b/tests/Constraints/UnionWithNullValueTest.php index a077cfdf..60301f2e 100644 --- a/tests/Constraints/UnionWithNullValueTest.php +++ b/tests/Constraints/UnionWithNullValueTest.php @@ -11,6 +11,8 @@ class UnionWithNullValueTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/UniqueItemsTest.php b/tests/Constraints/UniqueItemsTest.php index 4abac569..099b407c 100644 --- a/tests/Constraints/UniqueItemsTest.php +++ b/tests/Constraints/UniqueItemsTest.php @@ -11,6 +11,8 @@ class UniqueItemsTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/WrongMessagesFailingTestCaseTest.php b/tests/Constraints/WrongMessagesFailingTestCaseTest.php index ca620420..80a14421 100644 --- a/tests/Constraints/WrongMessagesFailingTestCaseTest.php +++ b/tests/Constraints/WrongMessagesFailingTestCaseTest.php @@ -11,6 +11,8 @@ class WrongMessagesFailingTestCaseTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Drafts/Draft3Test.php b/tests/Drafts/Draft3Test.php index 4a744441..1942a3b1 100644 --- a/tests/Drafts/Draft3Test.php +++ b/tests/Drafts/Draft3Test.php @@ -16,6 +16,9 @@ */ class Draft3Test extends BaseDraftTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + /** * {@inheritdoc} */ diff --git a/tests/Drafts/Draft4Test.php b/tests/Drafts/Draft4Test.php index a4508b0f..54eee4c4 100644 --- a/tests/Drafts/Draft4Test.php +++ b/tests/Drafts/Draft4Test.php @@ -14,6 +14,9 @@ */ class Draft4Test extends BaseDraftTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-04/schema#'; + protected $validateSchema = true; + /** * {@inheritdoc} */