| from __future__ import annotations |
|
|
| from collections import deque, namedtuple |
| from contextlib import contextmanager |
| from decimal import Decimal |
| from io import BytesIO |
| from typing import Any |
| from unittest import TestCase, mock |
| from urllib.request import pathname2url |
| import json |
| import os |
| import sys |
| import tempfile |
| import warnings |
|
|
| from attrs import define, field |
| from referencing.jsonschema import DRAFT202012 |
| import referencing.exceptions |
|
|
| from jsonschema import ( |
| FormatChecker, |
| TypeChecker, |
| exceptions, |
| protocols, |
| validators, |
| ) |
|
|
|
|
| def fail(validator, errors, instance, schema): |
| for each in errors: |
| each.setdefault("message", "You told me to fail!") |
| yield exceptions.ValidationError(**each) |
|
|
|
|
| class TestCreateAndExtend(TestCase): |
| def setUp(self): |
| self.addCleanup( |
| self.assertEqual, |
| validators._META_SCHEMAS, |
| dict(validators._META_SCHEMAS), |
| ) |
| self.addCleanup( |
| self.assertEqual, |
| validators._VALIDATORS, |
| dict(validators._VALIDATORS), |
| ) |
|
|
| self.meta_schema = {"$id": "some://meta/schema"} |
| self.validators = {"fail": fail} |
| self.type_checker = TypeChecker() |
| self.Validator = validators.create( |
| meta_schema=self.meta_schema, |
| validators=self.validators, |
| type_checker=self.type_checker, |
| ) |
|
|
| def test_attrs(self): |
| self.assertEqual( |
| ( |
| self.Validator.VALIDATORS, |
| self.Validator.META_SCHEMA, |
| self.Validator.TYPE_CHECKER, |
| ), ( |
| self.validators, |
| self.meta_schema, |
| self.type_checker, |
| ), |
| ) |
|
|
| def test_init(self): |
| schema = {"fail": []} |
| self.assertEqual(self.Validator(schema).schema, schema) |
|
|
| def test_iter_errors_successful(self): |
| schema = {"fail": []} |
| validator = self.Validator(schema) |
|
|
| errors = list(validator.iter_errors("hello")) |
| self.assertEqual(errors, []) |
|
|
| def test_iter_errors_one_error(self): |
| schema = {"fail": [{"message": "Whoops!"}]} |
| validator = self.Validator(schema) |
|
|
| expected_error = exceptions.ValidationError( |
| "Whoops!", |
| instance="goodbye", |
| schema=schema, |
| validator="fail", |
| validator_value=[{"message": "Whoops!"}], |
| schema_path=deque(["fail"]), |
| ) |
|
|
| errors = list(validator.iter_errors("goodbye")) |
| self.assertEqual(len(errors), 1) |
| self.assertEqual(errors[0]._contents(), expected_error._contents()) |
|
|
| def test_iter_errors_multiple_errors(self): |
| schema = { |
| "fail": [ |
| {"message": "First"}, |
| {"message": "Second!", "validator": "asdf"}, |
| {"message": "Third"}, |
| ], |
| } |
| validator = self.Validator(schema) |
|
|
| errors = list(validator.iter_errors("goodbye")) |
| self.assertEqual(len(errors), 3) |
|
|
| def test_if_a_version_is_provided_it_is_registered(self): |
| Validator = validators.create( |
| meta_schema={"$id": "something"}, |
| version="my version", |
| ) |
| self.addCleanup(validators._META_SCHEMAS.pop, "something") |
| self.addCleanup(validators._VALIDATORS.pop, "my version") |
| self.assertEqual(Validator.__name__, "MyVersionValidator") |
| self.assertEqual(Validator.__qualname__, "MyVersionValidator") |
|
|
| def test_repr(self): |
| Validator = validators.create( |
| meta_schema={"$id": "something"}, |
| version="my version", |
| ) |
| self.addCleanup(validators._META_SCHEMAS.pop, "something") |
| self.addCleanup(validators._VALIDATORS.pop, "my version") |
| self.assertEqual( |
| repr(Validator({})), |
| "MyVersionValidator(schema={}, format_checker=None)", |
| ) |
|
|
| def test_long_repr(self): |
| Validator = validators.create( |
| meta_schema={"$id": "something"}, |
| version="my version", |
| ) |
| self.addCleanup(validators._META_SCHEMAS.pop, "something") |
| self.addCleanup(validators._VALIDATORS.pop, "my version") |
| self.assertEqual( |
| repr(Validator({"a": list(range(1000))})), ( |
| "MyVersionValidator(schema={'a': [0, 1, 2, 3, 4, 5, ...]}, " |
| "format_checker=None)" |
| ), |
| ) |
|
|
| def test_repr_no_version(self): |
| Validator = validators.create(meta_schema={}) |
| self.assertEqual( |
| repr(Validator({})), |
| "Validator(schema={}, format_checker=None)", |
| ) |
|
|
| def test_dashes_are_stripped_from_validator_names(self): |
| Validator = validators.create( |
| meta_schema={"$id": "something"}, |
| version="foo-bar", |
| ) |
| self.addCleanup(validators._META_SCHEMAS.pop, "something") |
| self.addCleanup(validators._VALIDATORS.pop, "foo-bar") |
| self.assertEqual(Validator.__qualname__, "FooBarValidator") |
|
|
| def test_if_a_version_is_not_provided_it_is_not_registered(self): |
| original = dict(validators._META_SCHEMAS) |
| validators.create(meta_schema={"id": "id"}) |
| self.assertEqual(validators._META_SCHEMAS, original) |
|
|
| def test_validates_registers_meta_schema_id(self): |
| meta_schema_key = "meta schema id" |
| my_meta_schema = {"id": meta_schema_key} |
|
|
| validators.create( |
| meta_schema=my_meta_schema, |
| version="my version", |
| id_of=lambda s: s.get("id", ""), |
| ) |
| self.addCleanup(validators._META_SCHEMAS.pop, meta_schema_key) |
| self.addCleanup(validators._VALIDATORS.pop, "my version") |
|
|
| self.assertIn(meta_schema_key, validators._META_SCHEMAS) |
|
|
| def test_validates_registers_meta_schema_draft6_id(self): |
| meta_schema_key = "meta schema $id" |
| my_meta_schema = {"$id": meta_schema_key} |
|
|
| validators.create( |
| meta_schema=my_meta_schema, |
| version="my version", |
| ) |
| self.addCleanup(validators._META_SCHEMAS.pop, meta_schema_key) |
| self.addCleanup(validators._VALIDATORS.pop, "my version") |
|
|
| self.assertIn(meta_schema_key, validators._META_SCHEMAS) |
|
|
| def test_create_default_types(self): |
| Validator = validators.create(meta_schema={}, validators=()) |
| self.assertTrue( |
| all( |
| Validator({}).is_type(instance=instance, type=type) |
| for type, instance in [ |
| ("array", []), |
| ("boolean", True), |
| ("integer", 12), |
| ("null", None), |
| ("number", 12.0), |
| ("object", {}), |
| ("string", "foo"), |
| ] |
| ), |
| ) |
|
|
| def test_check_schema_with_different_metaschema(self): |
| """ |
| One can create a validator class whose metaschema uses a different |
| dialect than itself. |
| """ |
|
|
| NoEmptySchemasValidator = validators.create( |
| meta_schema={ |
| "$schema": validators.Draft202012Validator.META_SCHEMA["$id"], |
| "not": {"const": {}}, |
| }, |
| ) |
| NoEmptySchemasValidator.check_schema({"foo": "bar"}) |
|
|
| with self.assertRaises(exceptions.SchemaError): |
| NoEmptySchemasValidator.check_schema({}) |
|
|
| NoEmptySchemasValidator({"foo": "bar"}).validate("foo") |
|
|
| def test_check_schema_with_different_metaschema_defaults_to_self(self): |
| """ |
| A validator whose metaschema doesn't declare $schema defaults to its |
| own validation behavior, not the latest "normal" specification. |
| """ |
|
|
| NoEmptySchemasValidator = validators.create( |
| meta_schema={"fail": [{"message": "Meta schema whoops!"}]}, |
| validators={"fail": fail}, |
| ) |
| with self.assertRaises(exceptions.SchemaError): |
| NoEmptySchemasValidator.check_schema({}) |
|
|
| def test_extend(self): |
| original = dict(self.Validator.VALIDATORS) |
| new = object() |
|
|
| Extended = validators.extend( |
| self.Validator, |
| validators={"new": new}, |
| ) |
| self.assertEqual( |
| ( |
| Extended.VALIDATORS, |
| Extended.META_SCHEMA, |
| Extended.TYPE_CHECKER, |
| self.Validator.VALIDATORS, |
| ), ( |
| dict(original, new=new), |
| self.Validator.META_SCHEMA, |
| self.Validator.TYPE_CHECKER, |
| original, |
| ), |
| ) |
|
|
| def test_extend_idof(self): |
| """ |
| Extending a validator preserves its notion of schema IDs. |
| """ |
| def id_of(schema): |
| return schema.get("__test__", self.Validator.ID_OF(schema)) |
| correct_id = "the://correct/id/" |
| meta_schema = { |
| "$id": "the://wrong/id/", |
| "__test__": correct_id, |
| } |
| Original = validators.create( |
| meta_schema=meta_schema, |
| validators=self.validators, |
| type_checker=self.type_checker, |
| id_of=id_of, |
| ) |
| self.assertEqual(Original.ID_OF(Original.META_SCHEMA), correct_id) |
|
|
| Derived = validators.extend(Original) |
| self.assertEqual(Derived.ID_OF(Derived.META_SCHEMA), correct_id) |
|
|
| def test_extend_applicable_validators(self): |
| """ |
| Extending a validator preserves its notion of applicable validators. |
| """ |
|
|
| schema = { |
| "$defs": {"test": {"type": "number"}}, |
| "$ref": "#/$defs/test", |
| "maximum": 1, |
| } |
|
|
| draft4 = validators.Draft4Validator(schema) |
| self.assertTrue(draft4.is_valid(37)) |
|
|
| Derived = validators.extend(validators.Draft4Validator) |
| self.assertTrue(Derived(schema).is_valid(37)) |
|
|
|
|
| class TestValidationErrorMessages(TestCase): |
| def message_for(self, instance, schema, *args, **kwargs): |
| cls = kwargs.pop("cls", validators._LATEST_VERSION) |
| cls.check_schema(schema) |
| validator = cls(schema, *args, **kwargs) |
| errors = list(validator.iter_errors(instance)) |
| self.assertTrue(errors, msg=f"No errors were raised for {instance!r}") |
| self.assertEqual( |
| len(errors), |
| 1, |
| msg=f"Expected exactly one error, found {errors!r}", |
| ) |
| return errors[0].message |
|
|
| def test_single_type_failure(self): |
| message = self.message_for(instance=1, schema={"type": "string"}) |
| self.assertEqual(message, "1 is not of type 'string'") |
|
|
| def test_single_type_list_failure(self): |
| message = self.message_for(instance=1, schema={"type": ["string"]}) |
| self.assertEqual(message, "1 is not of type 'string'") |
|
|
| def test_multiple_type_failure(self): |
| types = "string", "object" |
| message = self.message_for(instance=1, schema={"type": list(types)}) |
| self.assertEqual(message, "1 is not of type 'string', 'object'") |
|
|
| def test_object_with_named_type_failure(self): |
| schema = {"type": [{"name": "Foo", "minimum": 3}]} |
| message = self.message_for( |
| instance=1, |
| schema=schema, |
| cls=validators.Draft3Validator, |
| ) |
| self.assertEqual(message, "1 is not of type 'Foo'") |
|
|
| def test_minimum(self): |
| message = self.message_for(instance=1, schema={"minimum": 2}) |
| self.assertEqual(message, "1 is less than the minimum of 2") |
|
|
| def test_maximum(self): |
| message = self.message_for(instance=1, schema={"maximum": 0}) |
| self.assertEqual(message, "1 is greater than the maximum of 0") |
|
|
| def test_dependencies_single_element(self): |
| depend, on = "bar", "foo" |
| schema = {"dependencies": {depend: on}} |
| message = self.message_for( |
| instance={"bar": 2}, |
| schema=schema, |
| cls=validators.Draft3Validator, |
| ) |
| self.assertEqual(message, "'foo' is a dependency of 'bar'") |
|
|
| def test_object_without_title_type_failure_draft3(self): |
| type = {"type": [{"minimum": 3}]} |
| message = self.message_for( |
| instance=1, |
| schema={"type": [type]}, |
| cls=validators.Draft3Validator, |
| ) |
| self.assertEqual( |
| message, |
| "1 is not of type {'type': [{'minimum': 3}]}", |
| ) |
|
|
| def test_dependencies_list_draft3(self): |
| depend, on = "bar", "foo" |
| schema = {"dependencies": {depend: [on]}} |
| message = self.message_for( |
| instance={"bar": 2}, |
| schema=schema, |
| cls=validators.Draft3Validator, |
| ) |
| self.assertEqual(message, "'foo' is a dependency of 'bar'") |
|
|
| def test_dependencies_list_draft7(self): |
| depend, on = "bar", "foo" |
| schema = {"dependencies": {depend: [on]}} |
| message = self.message_for( |
| instance={"bar": 2}, |
| schema=schema, |
| cls=validators.Draft7Validator, |
| ) |
| self.assertEqual(message, "'foo' is a dependency of 'bar'") |
|
|
| def test_additionalItems_single_failure(self): |
| message = self.message_for( |
| instance=[2], |
| schema={"items": [], "additionalItems": False}, |
| cls=validators.Draft3Validator, |
| ) |
| self.assertIn("(2 was unexpected)", message) |
|
|
| def test_additionalItems_multiple_failures(self): |
| message = self.message_for( |
| instance=[1, 2, 3], |
| schema={"items": [], "additionalItems": False}, |
| cls=validators.Draft3Validator, |
| ) |
| self.assertIn("(1, 2, 3 were unexpected)", message) |
|
|
| def test_additionalProperties_single_failure(self): |
| additional = "foo" |
| schema = {"additionalProperties": False} |
| message = self.message_for(instance={additional: 2}, schema=schema) |
| self.assertIn("('foo' was unexpected)", message) |
|
|
| def test_additionalProperties_multiple_failures(self): |
| schema = {"additionalProperties": False} |
| message = self.message_for( |
| instance=dict.fromkeys(["foo", "bar"]), |
| schema=schema, |
| ) |
|
|
| self.assertIn(repr("foo"), message) |
| self.assertIn(repr("bar"), message) |
| self.assertIn("were unexpected)", message) |
|
|
| def test_const(self): |
| schema = {"const": 12} |
| message = self.message_for( |
| instance={"foo": "bar"}, |
| schema=schema, |
| ) |
| self.assertIn("12 was expected", message) |
|
|
| def test_contains_draft_6(self): |
| schema = {"contains": {"const": 12}} |
| message = self.message_for( |
| instance=[2, {}, []], |
| schema=schema, |
| cls=validators.Draft6Validator, |
| ) |
| self.assertEqual( |
| message, |
| "None of [2, {}, []] are valid under the given schema", |
| ) |
|
|
| def test_invalid_format_default_message(self): |
| checker = FormatChecker(formats=()) |
| checker.checks("thing")(lambda value: False) |
|
|
| schema = {"format": "thing"} |
| message = self.message_for( |
| instance="bla", |
| schema=schema, |
| format_checker=checker, |
| ) |
|
|
| self.assertIn(repr("bla"), message) |
| self.assertIn(repr("thing"), message) |
| self.assertIn("is not a", message) |
|
|
| def test_additionalProperties_false_patternProperties(self): |
| schema = {"type": "object", |
| "additionalProperties": False, |
| "patternProperties": { |
| "^abc$": {"type": "string"}, |
| "^def$": {"type": "string"}, |
| }} |
| message = self.message_for( |
| instance={"zebra": 123}, |
| schema=schema, |
| cls=validators.Draft4Validator, |
| ) |
| self.assertEqual( |
| message, |
| "{} does not match any of the regexes: {}, {}".format( |
| repr("zebra"), repr("^abc$"), repr("^def$"), |
| ), |
| ) |
| message = self.message_for( |
| instance={"zebra": 123, "fish": 456}, |
| schema=schema, |
| cls=validators.Draft4Validator, |
| ) |
| self.assertEqual( |
| message, |
| "{}, {} do not match any of the regexes: {}, {}".format( |
| repr("fish"), repr("zebra"), repr("^abc$"), repr("^def$"), |
| ), |
| ) |
|
|
| def test_False_schema(self): |
| message = self.message_for( |
| instance="something", |
| schema=False, |
| ) |
| self.assertEqual(message, "False schema does not allow 'something'") |
|
|
| def test_multipleOf(self): |
| message = self.message_for( |
| instance=3, |
| schema={"multipleOf": 2}, |
| ) |
| self.assertEqual(message, "3 is not a multiple of 2") |
|
|
| def test_minItems(self): |
| message = self.message_for(instance=[], schema={"minItems": 2}) |
| self.assertEqual(message, "[] is too short") |
|
|
| def test_maxItems(self): |
| message = self.message_for(instance=[1, 2, 3], schema={"maxItems": 2}) |
| self.assertEqual(message, "[1, 2, 3] is too long") |
|
|
| def test_minItems_1(self): |
| message = self.message_for(instance=[], schema={"minItems": 1}) |
| self.assertEqual(message, "[] should be non-empty") |
|
|
| def test_maxItems_0(self): |
| message = self.message_for(instance=[1, 2, 3], schema={"maxItems": 0}) |
| self.assertEqual(message, "[1, 2, 3] is expected to be empty") |
|
|
| def test_minLength(self): |
| message = self.message_for( |
| instance="", |
| schema={"minLength": 2}, |
| ) |
| self.assertEqual(message, "'' is too short") |
|
|
| def test_maxLength(self): |
| message = self.message_for( |
| instance="abc", |
| schema={"maxLength": 2}, |
| ) |
| self.assertEqual(message, "'abc' is too long") |
|
|
| def test_minLength_1(self): |
| message = self.message_for(instance="", schema={"minLength": 1}) |
| self.assertEqual(message, "'' should be non-empty") |
|
|
| def test_maxLength_0(self): |
| message = self.message_for(instance="abc", schema={"maxLength": 0}) |
| self.assertEqual(message, "'abc' is expected to be empty") |
|
|
| def test_minProperties(self): |
| message = self.message_for(instance={}, schema={"minProperties": 2}) |
| self.assertEqual(message, "{} does not have enough properties") |
|
|
| def test_maxProperties(self): |
| message = self.message_for( |
| instance={"a": {}, "b": {}, "c": {}}, |
| schema={"maxProperties": 2}, |
| ) |
| self.assertEqual( |
| message, |
| "{'a': {}, 'b': {}, 'c': {}} has too many properties", |
| ) |
|
|
| def test_minProperties_1(self): |
| message = self.message_for(instance={}, schema={"minProperties": 1}) |
| self.assertEqual(message, "{} should be non-empty") |
|
|
| def test_maxProperties_0(self): |
| message = self.message_for( |
| instance={1: 2}, |
| schema={"maxProperties": 0}, |
| ) |
| self.assertEqual(message, "{1: 2} is expected to be empty") |
|
|
| def test_prefixItems_with_items(self): |
| message = self.message_for( |
| instance=[1, 2, "foo"], |
| schema={"items": False, "prefixItems": [{}, {}]}, |
| ) |
| self.assertEqual( |
| message, |
| "Expected at most 2 items but found 1 extra: 'foo'", |
| ) |
|
|
| def test_prefixItems_with_multiple_extra_items(self): |
| message = self.message_for( |
| instance=[1, 2, "foo", 5], |
| schema={"items": False, "prefixItems": [{}, {}]}, |
| ) |
| self.assertEqual( |
| message, |
| "Expected at most 2 items but found 2 extra: ['foo', 5]", |
| ) |
|
|
| def test_pattern(self): |
| message = self.message_for( |
| instance="bbb", |
| schema={"pattern": "^a*$"}, |
| ) |
| self.assertEqual(message, "'bbb' does not match '^a*$'") |
|
|
| def test_does_not_contain(self): |
| message = self.message_for( |
| instance=[], |
| schema={"contains": {"type": "string"}}, |
| ) |
| self.assertEqual( |
| message, |
| "[] does not contain items matching the given schema", |
| ) |
|
|
| def test_contains_too_few(self): |
| message = self.message_for( |
| instance=["foo", 1], |
| schema={"contains": {"type": "string"}, "minContains": 2}, |
| ) |
| self.assertEqual( |
| message, |
| "Too few items match the given schema " |
| "(expected at least 2 but only 1 matched)", |
| ) |
|
|
| def test_contains_too_few_both_constrained(self): |
| message = self.message_for( |
| instance=["foo", 1], |
| schema={ |
| "contains": {"type": "string"}, |
| "minContains": 2, |
| "maxContains": 4, |
| }, |
| ) |
| self.assertEqual( |
| message, |
| "Too few items match the given schema (expected at least 2 but " |
| "only 1 matched)", |
| ) |
|
|
| def test_contains_too_many(self): |
| message = self.message_for( |
| instance=["foo", "bar", "baz"], |
| schema={"contains": {"type": "string"}, "maxContains": 2}, |
| ) |
| self.assertEqual( |
| message, |
| "Too many items match the given schema (expected at most 2)", |
| ) |
|
|
| def test_contains_too_many_both_constrained(self): |
| message = self.message_for( |
| instance=["foo"] * 5, |
| schema={ |
| "contains": {"type": "string"}, |
| "minContains": 2, |
| "maxContains": 4, |
| }, |
| ) |
| self.assertEqual( |
| message, |
| "Too many items match the given schema (expected at most 4)", |
| ) |
|
|
| def test_exclusiveMinimum(self): |
| message = self.message_for( |
| instance=3, |
| schema={"exclusiveMinimum": 5}, |
| ) |
| self.assertEqual( |
| message, |
| "3 is less than or equal to the minimum of 5", |
| ) |
|
|
| def test_exclusiveMaximum(self): |
| message = self.message_for(instance=3, schema={"exclusiveMaximum": 2}) |
| self.assertEqual( |
| message, |
| "3 is greater than or equal to the maximum of 2", |
| ) |
|
|
| def test_required(self): |
| message = self.message_for(instance={}, schema={"required": ["foo"]}) |
| self.assertEqual(message, "'foo' is a required property") |
|
|
| def test_dependentRequired(self): |
| message = self.message_for( |
| instance={"foo": {}}, |
| schema={"dependentRequired": {"foo": ["bar"]}}, |
| ) |
| self.assertEqual(message, "'bar' is a dependency of 'foo'") |
|
|
| def test_oneOf_matches_none(self): |
| message = self.message_for(instance={}, schema={"oneOf": [False]}) |
| self.assertEqual( |
| message, |
| "{} is not valid under any of the given schemas", |
| ) |
|
|
| def test_oneOf_matches_too_many(self): |
| message = self.message_for(instance={}, schema={"oneOf": [True, True]}) |
| self.assertEqual(message, "{} is valid under each of True, True") |
|
|
| def test_unevaluated_items(self): |
| schema = {"type": "array", "unevaluatedItems": False} |
| message = self.message_for(instance=["foo", "bar"], schema=schema) |
| self.assertIn( |
| message, |
| "Unevaluated items are not allowed ('foo', 'bar' were unexpected)", |
| ) |
|
|
| def test_unevaluated_items_on_invalid_type(self): |
| schema = {"type": "array", "unevaluatedItems": False} |
| message = self.message_for(instance="foo", schema=schema) |
| self.assertEqual(message, "'foo' is not of type 'array'") |
|
|
| def test_unevaluated_properties_invalid_against_subschema(self): |
| schema = { |
| "properties": {"foo": {"type": "string"}}, |
| "unevaluatedProperties": {"const": 12}, |
| } |
| message = self.message_for( |
| instance={ |
| "foo": "foo", |
| "bar": "bar", |
| "baz": 12, |
| }, |
| schema=schema, |
| ) |
| self.assertEqual( |
| message, |
| "Unevaluated properties are not valid under the given schema " |
| "('bar' was unevaluated and invalid)", |
| ) |
|
|
| def test_unevaluated_properties_disallowed(self): |
| schema = {"type": "object", "unevaluatedProperties": False} |
| message = self.message_for( |
| instance={ |
| "foo": "foo", |
| "bar": "bar", |
| }, |
| schema=schema, |
| ) |
| self.assertEqual( |
| message, |
| "Unevaluated properties are not allowed " |
| "('bar', 'foo' were unexpected)", |
| ) |
|
|
| def test_unevaluated_properties_on_invalid_type(self): |
| schema = {"type": "object", "unevaluatedProperties": False} |
| message = self.message_for(instance="foo", schema=schema) |
| self.assertEqual(message, "'foo' is not of type 'object'") |
|
|
| def test_single_item(self): |
| schema = {"prefixItems": [{}], "items": False} |
| message = self.message_for( |
| instance=["foo", "bar", "baz"], |
| schema=schema, |
| ) |
| self.assertEqual( |
| message, |
| "Expected at most 1 item but found 2 extra: ['bar', 'baz']", |
| ) |
|
|
| def test_heterogeneous_additionalItems_with_Items(self): |
| schema = {"items": [{}], "additionalItems": False} |
| message = self.message_for( |
| instance=["foo", "bar", 37], |
| schema=schema, |
| cls=validators.Draft7Validator, |
| ) |
| self.assertEqual( |
| message, |
| "Additional items are not allowed ('bar', 37 were unexpected)", |
| ) |
|
|
| def test_heterogeneous_items_prefixItems(self): |
| schema = {"prefixItems": [{}], "items": False} |
| message = self.message_for( |
| instance=["foo", "bar", 37], |
| schema=schema, |
| ) |
| self.assertEqual( |
| message, |
| "Expected at most 1 item but found 2 extra: ['bar', 37]", |
| ) |
|
|
| def test_heterogeneous_unevaluatedItems_prefixItems(self): |
| schema = {"prefixItems": [{}], "unevaluatedItems": False} |
| message = self.message_for( |
| instance=["foo", "bar", 37], |
| schema=schema, |
| ) |
| self.assertEqual( |
| message, |
| "Unevaluated items are not allowed ('bar', 37 were unexpected)", |
| ) |
|
|
| def test_heterogeneous_properties_additionalProperties(self): |
| """ |
| Not valid deserialized JSON, but this should not blow up. |
| """ |
| schema = {"properties": {"foo": {}}, "additionalProperties": False} |
| message = self.message_for( |
| instance={"foo": {}, "a": "baz", 37: 12}, |
| schema=schema, |
| ) |
| self.assertEqual( |
| message, |
| "Additional properties are not allowed (37, 'a' were unexpected)", |
| ) |
|
|
| def test_heterogeneous_properties_unevaluatedProperties(self): |
| """ |
| Not valid deserialized JSON, but this should not blow up. |
| """ |
| schema = {"properties": {"foo": {}}, "unevaluatedProperties": False} |
| message = self.message_for( |
| instance={"foo": {}, "a": "baz", 37: 12}, |
| schema=schema, |
| ) |
| self.assertEqual( |
| message, |
| "Unevaluated properties are not allowed (37, 'a' were unexpected)", |
| ) |
|
|
|
|
| class TestValidationErrorDetails(TestCase): |
| |
| |
| def test_anyOf(self): |
| instance = 5 |
| schema = { |
| "anyOf": [ |
| {"minimum": 20}, |
| {"type": "string"}, |
| ], |
| } |
|
|
| validator = validators.Draft4Validator(schema) |
| errors = list(validator.iter_errors(instance)) |
| self.assertEqual(len(errors), 1) |
| e = errors[0] |
|
|
| self.assertEqual(e.validator, "anyOf") |
| self.assertEqual(e.validator_value, schema["anyOf"]) |
| self.assertEqual(e.instance, instance) |
| self.assertEqual(e.schema, schema) |
| self.assertIsNone(e.parent) |
|
|
| self.assertEqual(e.path, deque([])) |
| self.assertEqual(e.relative_path, deque([])) |
| self.assertEqual(e.absolute_path, deque([])) |
| self.assertEqual(e.json_path, "$") |
|
|
| self.assertEqual(e.schema_path, deque(["anyOf"])) |
| self.assertEqual(e.relative_schema_path, deque(["anyOf"])) |
| self.assertEqual(e.absolute_schema_path, deque(["anyOf"])) |
|
|
| self.assertEqual(len(e.context), 2) |
|
|
| e1, e2 = sorted_errors(e.context) |
|
|
| self.assertEqual(e1.validator, "minimum") |
| self.assertEqual(e1.validator_value, schema["anyOf"][0]["minimum"]) |
| self.assertEqual(e1.instance, instance) |
| self.assertEqual(e1.schema, schema["anyOf"][0]) |
| self.assertIs(e1.parent, e) |
|
|
| self.assertEqual(e1.path, deque([])) |
| self.assertEqual(e1.absolute_path, deque([])) |
| self.assertEqual(e1.relative_path, deque([])) |
| self.assertEqual(e1.json_path, "$") |
|
|
| self.assertEqual(e1.schema_path, deque([0, "minimum"])) |
| self.assertEqual(e1.relative_schema_path, deque([0, "minimum"])) |
| self.assertEqual( |
| e1.absolute_schema_path, deque(["anyOf", 0, "minimum"]), |
| ) |
|
|
| self.assertFalse(e1.context) |
|
|
| self.assertEqual(e2.validator, "type") |
| self.assertEqual(e2.validator_value, schema["anyOf"][1]["type"]) |
| self.assertEqual(e2.instance, instance) |
| self.assertEqual(e2.schema, schema["anyOf"][1]) |
| self.assertIs(e2.parent, e) |
|
|
| self.assertEqual(e2.path, deque([])) |
| self.assertEqual(e2.relative_path, deque([])) |
| self.assertEqual(e2.absolute_path, deque([])) |
| self.assertEqual(e2.json_path, "$") |
|
|
| self.assertEqual(e2.schema_path, deque([1, "type"])) |
| self.assertEqual(e2.relative_schema_path, deque([1, "type"])) |
| self.assertEqual(e2.absolute_schema_path, deque(["anyOf", 1, "type"])) |
|
|
| self.assertEqual(len(e2.context), 0) |
|
|
| def test_type(self): |
| instance = {"foo": 1} |
| schema = { |
| "type": [ |
| {"type": "integer"}, |
| { |
| "type": "object", |
| "properties": {"foo": {"enum": [2]}}, |
| }, |
| ], |
| } |
|
|
| validator = validators.Draft3Validator(schema) |
| errors = list(validator.iter_errors(instance)) |
| self.assertEqual(len(errors), 1) |
| e = errors[0] |
|
|
| self.assertEqual(e.validator, "type") |
| self.assertEqual(e.validator_value, schema["type"]) |
| self.assertEqual(e.instance, instance) |
| self.assertEqual(e.schema, schema) |
| self.assertIsNone(e.parent) |
|
|
| self.assertEqual(e.path, deque([])) |
| self.assertEqual(e.relative_path, deque([])) |
| self.assertEqual(e.absolute_path, deque([])) |
| self.assertEqual(e.json_path, "$") |
|
|
| self.assertEqual(e.schema_path, deque(["type"])) |
| self.assertEqual(e.relative_schema_path, deque(["type"])) |
| self.assertEqual(e.absolute_schema_path, deque(["type"])) |
|
|
| self.assertEqual(len(e.context), 2) |
|
|
| e1, e2 = sorted_errors(e.context) |
|
|
| self.assertEqual(e1.validator, "type") |
| self.assertEqual(e1.validator_value, schema["type"][0]["type"]) |
| self.assertEqual(e1.instance, instance) |
| self.assertEqual(e1.schema, schema["type"][0]) |
| self.assertIs(e1.parent, e) |
|
|
| self.assertEqual(e1.path, deque([])) |
| self.assertEqual(e1.relative_path, deque([])) |
| self.assertEqual(e1.absolute_path, deque([])) |
| self.assertEqual(e1.json_path, "$") |
|
|
| self.assertEqual(e1.schema_path, deque([0, "type"])) |
| self.assertEqual(e1.relative_schema_path, deque([0, "type"])) |
| self.assertEqual(e1.absolute_schema_path, deque(["type", 0, "type"])) |
|
|
| self.assertFalse(e1.context) |
|
|
| self.assertEqual(e2.validator, "enum") |
| self.assertEqual(e2.validator_value, [2]) |
| self.assertEqual(e2.instance, 1) |
| self.assertEqual(e2.schema, {"enum": [2]}) |
| self.assertIs(e2.parent, e) |
|
|
| self.assertEqual(e2.path, deque(["foo"])) |
| self.assertEqual(e2.relative_path, deque(["foo"])) |
| self.assertEqual(e2.absolute_path, deque(["foo"])) |
| self.assertEqual(e2.json_path, "$.foo") |
|
|
| self.assertEqual( |
| e2.schema_path, deque([1, "properties", "foo", "enum"]), |
| ) |
| self.assertEqual( |
| e2.relative_schema_path, deque([1, "properties", "foo", "enum"]), |
| ) |
| self.assertEqual( |
| e2.absolute_schema_path, |
| deque(["type", 1, "properties", "foo", "enum"]), |
| ) |
|
|
| self.assertFalse(e2.context) |
|
|
| def test_single_nesting(self): |
| instance = {"foo": 2, "bar": [1], "baz": 15, "quux": "spam"} |
| schema = { |
| "properties": { |
| "foo": {"type": "string"}, |
| "bar": {"minItems": 2}, |
| "baz": {"maximum": 10, "enum": [2, 4, 6, 8]}, |
| }, |
| } |
|
|
| validator = validators.Draft3Validator(schema) |
| errors = validator.iter_errors(instance) |
| e1, e2, e3, e4 = sorted_errors(errors) |
|
|
| self.assertEqual(e1.path, deque(["bar"])) |
| self.assertEqual(e2.path, deque(["baz"])) |
| self.assertEqual(e3.path, deque(["baz"])) |
| self.assertEqual(e4.path, deque(["foo"])) |
|
|
| self.assertEqual(e1.relative_path, deque(["bar"])) |
| self.assertEqual(e2.relative_path, deque(["baz"])) |
| self.assertEqual(e3.relative_path, deque(["baz"])) |
| self.assertEqual(e4.relative_path, deque(["foo"])) |
|
|
| self.assertEqual(e1.absolute_path, deque(["bar"])) |
| self.assertEqual(e2.absolute_path, deque(["baz"])) |
| self.assertEqual(e3.absolute_path, deque(["baz"])) |
| self.assertEqual(e4.absolute_path, deque(["foo"])) |
|
|
| self.assertEqual(e1.json_path, "$.bar") |
| self.assertEqual(e2.json_path, "$.baz") |
| self.assertEqual(e3.json_path, "$.baz") |
| self.assertEqual(e4.json_path, "$.foo") |
|
|
| self.assertEqual(e1.validator, "minItems") |
| self.assertEqual(e2.validator, "enum") |
| self.assertEqual(e3.validator, "maximum") |
| self.assertEqual(e4.validator, "type") |
|
|
| def test_multiple_nesting(self): |
| instance = [1, {"foo": 2, "bar": {"baz": [1]}}, "quux"] |
| schema = { |
| "type": "string", |
| "items": { |
| "type": ["string", "object"], |
| "properties": { |
| "foo": {"enum": [1, 3]}, |
| "bar": { |
| "type": "array", |
| "properties": { |
| "bar": {"required": True}, |
| "baz": {"minItems": 2}, |
| }, |
| }, |
| }, |
| }, |
| } |
|
|
| validator = validators.Draft3Validator(schema) |
| errors = validator.iter_errors(instance) |
| e1, e2, e3, e4, e5, e6 = sorted_errors(errors) |
|
|
| self.assertEqual(e1.path, deque([])) |
| self.assertEqual(e2.path, deque([0])) |
| self.assertEqual(e3.path, deque([1, "bar"])) |
| self.assertEqual(e4.path, deque([1, "bar", "bar"])) |
| self.assertEqual(e5.path, deque([1, "bar", "baz"])) |
| self.assertEqual(e6.path, deque([1, "foo"])) |
|
|
| self.assertEqual(e1.json_path, "$") |
| self.assertEqual(e2.json_path, "$[0]") |
| self.assertEqual(e3.json_path, "$[1].bar") |
| self.assertEqual(e4.json_path, "$[1].bar.bar") |
| self.assertEqual(e5.json_path, "$[1].bar.baz") |
| self.assertEqual(e6.json_path, "$[1].foo") |
|
|
| self.assertEqual(e1.schema_path, deque(["type"])) |
| self.assertEqual(e2.schema_path, deque(["items", "type"])) |
| self.assertEqual( |
| list(e3.schema_path), ["items", "properties", "bar", "type"], |
| ) |
| self.assertEqual( |
| list(e4.schema_path), |
| ["items", "properties", "bar", "properties", "bar", "required"], |
| ) |
| self.assertEqual( |
| list(e5.schema_path), |
| ["items", "properties", "bar", "properties", "baz", "minItems"], |
| ) |
| self.assertEqual( |
| list(e6.schema_path), ["items", "properties", "foo", "enum"], |
| ) |
|
|
| self.assertEqual(e1.validator, "type") |
| self.assertEqual(e2.validator, "type") |
| self.assertEqual(e3.validator, "type") |
| self.assertEqual(e4.validator, "required") |
| self.assertEqual(e5.validator, "minItems") |
| self.assertEqual(e6.validator, "enum") |
|
|
| def test_recursive(self): |
| schema = { |
| "definitions": { |
| "node": { |
| "anyOf": [{ |
| "type": "object", |
| "required": ["name", "children"], |
| "properties": { |
| "name": { |
| "type": "string", |
| }, |
| "children": { |
| "type": "object", |
| "patternProperties": { |
| "^.*$": { |
| "$ref": "#/definitions/node", |
| }, |
| }, |
| }, |
| }, |
| }], |
| }, |
| }, |
| "type": "object", |
| "required": ["root"], |
| "properties": {"root": {"$ref": "#/definitions/node"}}, |
| } |
|
|
| instance = { |
| "root": { |
| "name": "root", |
| "children": { |
| "a": { |
| "name": "a", |
| "children": { |
| "ab": { |
| "name": "ab", |
| |
| }, |
| }, |
| }, |
| }, |
| }, |
| } |
| validator = validators.Draft4Validator(schema) |
|
|
| e, = validator.iter_errors(instance) |
| self.assertEqual(e.absolute_path, deque(["root"])) |
| self.assertEqual( |
| e.absolute_schema_path, deque(["properties", "root", "anyOf"]), |
| ) |
| self.assertEqual(e.json_path, "$.root") |
|
|
| e1, = e.context |
| self.assertEqual(e1.absolute_path, deque(["root", "children", "a"])) |
| self.assertEqual( |
| e1.absolute_schema_path, deque( |
| [ |
| "properties", |
| "root", |
| "anyOf", |
| 0, |
| "properties", |
| "children", |
| "patternProperties", |
| "^.*$", |
| "anyOf", |
| ], |
| ), |
| ) |
| self.assertEqual(e1.json_path, "$.root.children.a") |
|
|
| e2, = e1.context |
| self.assertEqual( |
| e2.absolute_path, deque( |
| ["root", "children", "a", "children", "ab"], |
| ), |
| ) |
| self.assertEqual( |
| e2.absolute_schema_path, deque( |
| [ |
| "properties", |
| "root", |
| "anyOf", |
| 0, |
| "properties", |
| "children", |
| "patternProperties", |
| "^.*$", |
| "anyOf", |
| 0, |
| "properties", |
| "children", |
| "patternProperties", |
| "^.*$", |
| "anyOf", |
| ], |
| ), |
| ) |
| self.assertEqual(e2.json_path, "$.root.children.a.children.ab") |
|
|
| def test_additionalProperties(self): |
| instance = {"bar": "bar", "foo": 2} |
| schema = {"additionalProperties": {"type": "integer", "minimum": 5}} |
|
|
| validator = validators.Draft3Validator(schema) |
| errors = validator.iter_errors(instance) |
| e1, e2 = sorted_errors(errors) |
|
|
| self.assertEqual(e1.path, deque(["bar"])) |
| self.assertEqual(e2.path, deque(["foo"])) |
|
|
| self.assertEqual(e1.json_path, "$.bar") |
| self.assertEqual(e2.json_path, "$.foo") |
|
|
| self.assertEqual(e1.validator, "type") |
| self.assertEqual(e2.validator, "minimum") |
|
|
| def test_patternProperties(self): |
| instance = {"bar": 1, "foo": 2} |
| schema = { |
| "patternProperties": { |
| "bar": {"type": "string"}, |
| "foo": {"minimum": 5}, |
| }, |
| } |
|
|
| validator = validators.Draft3Validator(schema) |
| errors = validator.iter_errors(instance) |
| e1, e2 = sorted_errors(errors) |
|
|
| self.assertEqual(e1.path, deque(["bar"])) |
| self.assertEqual(e2.path, deque(["foo"])) |
|
|
| self.assertEqual(e1.json_path, "$.bar") |
| self.assertEqual(e2.json_path, "$.foo") |
|
|
| self.assertEqual(e1.validator, "type") |
| self.assertEqual(e2.validator, "minimum") |
|
|
| def test_additionalItems(self): |
| instance = ["foo", 1] |
| schema = { |
| "items": [], |
| "additionalItems": {"type": "integer", "minimum": 5}, |
| } |
|
|
| validator = validators.Draft3Validator(schema) |
| errors = validator.iter_errors(instance) |
| e1, e2 = sorted_errors(errors) |
|
|
| self.assertEqual(e1.path, deque([0])) |
| self.assertEqual(e2.path, deque([1])) |
|
|
| self.assertEqual(e1.json_path, "$[0]") |
| self.assertEqual(e2.json_path, "$[1]") |
|
|
| self.assertEqual(e1.validator, "type") |
| self.assertEqual(e2.validator, "minimum") |
|
|
| def test_additionalItems_with_items(self): |
| instance = ["foo", "bar", 1] |
| schema = { |
| "items": [{}], |
| "additionalItems": {"type": "integer", "minimum": 5}, |
| } |
|
|
| validator = validators.Draft3Validator(schema) |
| errors = validator.iter_errors(instance) |
| e1, e2 = sorted_errors(errors) |
|
|
| self.assertEqual(e1.path, deque([1])) |
| self.assertEqual(e2.path, deque([2])) |
|
|
| self.assertEqual(e1.json_path, "$[1]") |
| self.assertEqual(e2.json_path, "$[2]") |
|
|
| self.assertEqual(e1.validator, "type") |
| self.assertEqual(e2.validator, "minimum") |
|
|
| def test_propertyNames(self): |
| instance = {"foo": 12} |
| schema = {"propertyNames": {"not": {"const": "foo"}}} |
|
|
| validator = validators.Draft7Validator(schema) |
| error, = validator.iter_errors(instance) |
|
|
| self.assertEqual(error.validator, "not") |
| self.assertEqual( |
| error.message, |
| "'foo' should not be valid under {'const': 'foo'}", |
| ) |
| self.assertEqual(error.path, deque([])) |
| self.assertEqual(error.json_path, "$") |
| self.assertEqual(error.schema_path, deque(["propertyNames", "not"])) |
|
|
| def test_if_then(self): |
| schema = { |
| "if": {"const": 12}, |
| "then": {"const": 13}, |
| } |
|
|
| validator = validators.Draft7Validator(schema) |
| error, = validator.iter_errors(12) |
|
|
| self.assertEqual(error.validator, "const") |
| self.assertEqual(error.message, "13 was expected") |
| self.assertEqual(error.path, deque([])) |
| self.assertEqual(error.json_path, "$") |
| self.assertEqual(error.schema_path, deque(["then", "const"])) |
|
|
| def test_if_else(self): |
| schema = { |
| "if": {"const": 12}, |
| "else": {"const": 13}, |
| } |
|
|
| validator = validators.Draft7Validator(schema) |
| error, = validator.iter_errors(15) |
|
|
| self.assertEqual(error.validator, "const") |
| self.assertEqual(error.message, "13 was expected") |
| self.assertEqual(error.path, deque([])) |
| self.assertEqual(error.json_path, "$") |
| self.assertEqual(error.schema_path, deque(["else", "const"])) |
|
|
| def test_boolean_schema_False(self): |
| validator = validators.Draft7Validator(False) |
| error, = validator.iter_errors(12) |
|
|
| self.assertEqual( |
| ( |
| error.message, |
| error.validator, |
| error.validator_value, |
| error.instance, |
| error.schema, |
| error.schema_path, |
| error.json_path, |
| ), |
| ( |
| "False schema does not allow 12", |
| None, |
| None, |
| 12, |
| False, |
| deque([]), |
| "$", |
| ), |
| ) |
|
|
| def test_ref(self): |
| ref, schema = "someRef", {"additionalProperties": {"type": "integer"}} |
| validator = validators.Draft7Validator( |
| {"$ref": ref}, |
| resolver=validators._RefResolver("", {}, store={ref: schema}), |
| ) |
| error, = validator.iter_errors({"foo": "notAnInteger"}) |
|
|
| self.assertEqual( |
| ( |
| error.message, |
| error.validator, |
| error.validator_value, |
| error.instance, |
| error.absolute_path, |
| error.schema, |
| error.schema_path, |
| error.json_path, |
| ), |
| ( |
| "'notAnInteger' is not of type 'integer'", |
| "type", |
| "integer", |
| "notAnInteger", |
| deque(["foo"]), |
| {"type": "integer"}, |
| deque(["additionalProperties", "type"]), |
| "$.foo", |
| ), |
| ) |
|
|
| def test_prefixItems(self): |
| schema = {"prefixItems": [{"type": "string"}, {}, {}, {"maximum": 3}]} |
| validator = validators.Draft202012Validator(schema) |
| type_error, min_error = validator.iter_errors([1, 2, "foo", 5]) |
| self.assertEqual( |
| ( |
| type_error.message, |
| type_error.validator, |
| type_error.validator_value, |
| type_error.instance, |
| type_error.absolute_path, |
| type_error.schema, |
| type_error.schema_path, |
| type_error.json_path, |
| ), |
| ( |
| "1 is not of type 'string'", |
| "type", |
| "string", |
| 1, |
| deque([0]), |
| {"type": "string"}, |
| deque(["prefixItems", 0, "type"]), |
| "$[0]", |
| ), |
| ) |
| self.assertEqual( |
| ( |
| min_error.message, |
| min_error.validator, |
| min_error.validator_value, |
| min_error.instance, |
| min_error.absolute_path, |
| min_error.schema, |
| min_error.schema_path, |
| min_error.json_path, |
| ), |
| ( |
| "5 is greater than the maximum of 3", |
| "maximum", |
| 3, |
| 5, |
| deque([3]), |
| {"maximum": 3}, |
| deque(["prefixItems", 3, "maximum"]), |
| "$[3]", |
| ), |
| ) |
|
|
| def test_prefixItems_with_items(self): |
| schema = { |
| "items": {"type": "string"}, |
| "prefixItems": [{}], |
| } |
| validator = validators.Draft202012Validator(schema) |
| e1, e2 = validator.iter_errors(["foo", 2, "bar", 4, "baz"]) |
| self.assertEqual( |
| ( |
| e1.message, |
| e1.validator, |
| e1.validator_value, |
| e1.instance, |
| e1.absolute_path, |
| e1.schema, |
| e1.schema_path, |
| e1.json_path, |
| ), |
| ( |
| "2 is not of type 'string'", |
| "type", |
| "string", |
| 2, |
| deque([1]), |
| {"type": "string"}, |
| deque(["items", "type"]), |
| "$[1]", |
| ), |
| ) |
| self.assertEqual( |
| ( |
| e2.message, |
| e2.validator, |
| e2.validator_value, |
| e2.instance, |
| e2.absolute_path, |
| e2.schema, |
| e2.schema_path, |
| e2.json_path, |
| ), |
| ( |
| "4 is not of type 'string'", |
| "type", |
| "string", |
| 4, |
| deque([3]), |
| {"type": "string"}, |
| deque(["items", "type"]), |
| "$[3]", |
| ), |
| ) |
|
|
| def test_contains_too_many(self): |
| """ |
| `contains` + `maxContains` produces only one error, even if there are |
| many more incorrectly matching elements. |
| """ |
| schema = {"contains": {"type": "string"}, "maxContains": 2} |
| validator = validators.Draft202012Validator(schema) |
| error, = validator.iter_errors(["foo", 2, "bar", 4, "baz", "quux"]) |
| self.assertEqual( |
| ( |
| error.message, |
| error.validator, |
| error.validator_value, |
| error.instance, |
| error.absolute_path, |
| error.schema, |
| error.schema_path, |
| error.json_path, |
| ), |
| ( |
| "Too many items match the given schema (expected at most 2)", |
| "maxContains", |
| 2, |
| ["foo", 2, "bar", 4, "baz", "quux"], |
| deque([]), |
| {"contains": {"type": "string"}, "maxContains": 2}, |
| deque(["contains"]), |
| "$", |
| ), |
| ) |
|
|
| def test_contains_too_few(self): |
| schema = {"contains": {"type": "string"}, "minContains": 2} |
| validator = validators.Draft202012Validator(schema) |
| error, = validator.iter_errors(["foo", 2, 4]) |
| self.assertEqual( |
| ( |
| error.message, |
| error.validator, |
| error.validator_value, |
| error.instance, |
| error.absolute_path, |
| error.schema, |
| error.schema_path, |
| error.json_path, |
| ), |
| ( |
| ( |
| "Too few items match the given schema " |
| "(expected at least 2 but only 1 matched)" |
| ), |
| "minContains", |
| 2, |
| ["foo", 2, 4], |
| deque([]), |
| {"contains": {"type": "string"}, "minContains": 2}, |
| deque(["contains"]), |
| "$", |
| ), |
| ) |
|
|
| def test_contains_none(self): |
| schema = {"contains": {"type": "string"}, "minContains": 2} |
| validator = validators.Draft202012Validator(schema) |
| error, = validator.iter_errors([2, 4]) |
| self.assertEqual( |
| ( |
| error.message, |
| error.validator, |
| error.validator_value, |
| error.instance, |
| error.absolute_path, |
| error.schema, |
| error.schema_path, |
| error.json_path, |
| ), |
| ( |
| "[2, 4] does not contain items matching the given schema", |
| "contains", |
| {"type": "string"}, |
| [2, 4], |
| deque([]), |
| {"contains": {"type": "string"}, "minContains": 2}, |
| deque(["contains"]), |
| "$", |
| ), |
| ) |
|
|
| def test_ref_sibling(self): |
| schema = { |
| "$defs": {"foo": {"required": ["bar"]}}, |
| "properties": { |
| "aprop": { |
| "$ref": "#/$defs/foo", |
| "required": ["baz"], |
| }, |
| }, |
| } |
|
|
| validator = validators.Draft202012Validator(schema) |
| e1, e2 = validator.iter_errors({"aprop": {}}) |
| self.assertEqual( |
| ( |
| e1.message, |
| e1.validator, |
| e1.validator_value, |
| e1.instance, |
| e1.absolute_path, |
| e1.schema, |
| e1.schema_path, |
| e1.relative_schema_path, |
| e1.json_path, |
| ), |
| ( |
| "'bar' is a required property", |
| "required", |
| ["bar"], |
| {}, |
| deque(["aprop"]), |
| {"required": ["bar"]}, |
| deque(["properties", "aprop", "required"]), |
| deque(["properties", "aprop", "required"]), |
| "$.aprop", |
| ), |
| ) |
| self.assertEqual( |
| ( |
| e2.message, |
| e2.validator, |
| e2.validator_value, |
| e2.instance, |
| e2.absolute_path, |
| e2.schema, |
| e2.schema_path, |
| e2.relative_schema_path, |
| e2.json_path, |
| ), |
| ( |
| "'baz' is a required property", |
| "required", |
| ["baz"], |
| {}, |
| deque(["aprop"]), |
| {"$ref": "#/$defs/foo", "required": ["baz"]}, |
| deque(["properties", "aprop", "required"]), |
| deque(["properties", "aprop", "required"]), |
| "$.aprop", |
| ), |
| ) |
|
|
|
|
| class MetaSchemaTestsMixin: |
| |
| def test_invalid_properties(self): |
| with self.assertRaises(exceptions.SchemaError): |
| self.Validator.check_schema({"properties": 12}) |
|
|
| def test_minItems_invalid_string(self): |
| with self.assertRaises(exceptions.SchemaError): |
| |
| self.Validator.check_schema({"minItems": "1"}) |
|
|
| def test_enum_allows_empty_arrays(self): |
| """ |
| Technically, all the spec says is they SHOULD have elements, not MUST. |
| |
| (As of Draft 6. Previous drafts do say MUST). |
| |
| See #529. |
| """ |
| if self.Validator in { |
| validators.Draft3Validator, |
| validators.Draft4Validator, |
| }: |
| with self.assertRaises(exceptions.SchemaError): |
| self.Validator.check_schema({"enum": []}) |
| else: |
| self.Validator.check_schema({"enum": []}) |
|
|
| def test_enum_allows_non_unique_items(self): |
| """ |
| Technically, all the spec says is they SHOULD be unique, not MUST. |
| |
| (As of Draft 6. Previous drafts do say MUST). |
| |
| See #529. |
| """ |
| if self.Validator in { |
| validators.Draft3Validator, |
| validators.Draft4Validator, |
| }: |
| with self.assertRaises(exceptions.SchemaError): |
| self.Validator.check_schema({"enum": [12, 12]}) |
| else: |
| self.Validator.check_schema({"enum": [12, 12]}) |
|
|
| def test_schema_with_invalid_regex(self): |
| with self.assertRaises(exceptions.SchemaError): |
| self.Validator.check_schema({"pattern": "*notaregex"}) |
|
|
| def test_schema_with_invalid_regex_with_disabled_format_validation(self): |
| self.Validator.check_schema( |
| {"pattern": "*notaregex"}, |
| format_checker=None, |
| ) |
|
|
|
|
| class ValidatorTestMixin(MetaSchemaTestsMixin): |
| def test_it_implements_the_validator_protocol(self): |
| self.assertIsInstance(self.Validator({}), protocols.Validator) |
|
|
| def test_valid_instances_are_valid(self): |
| schema, instance = self.valid |
| self.assertTrue(self.Validator(schema).is_valid(instance)) |
|
|
| def test_invalid_instances_are_not_valid(self): |
| schema, instance = self.invalid |
| self.assertFalse(self.Validator(schema).is_valid(instance)) |
|
|
| def test_non_existent_properties_are_ignored(self): |
| self.Validator({object(): object()}).validate(instance=object()) |
|
|
| def test_evolve(self): |
| schema, format_checker = {"type": "integer"}, FormatChecker() |
| original = self.Validator( |
| schema, |
| format_checker=format_checker, |
| ) |
| new = original.evolve( |
| schema={"type": "string"}, |
| format_checker=self.Validator.FORMAT_CHECKER, |
| ) |
|
|
| expected = self.Validator( |
| {"type": "string"}, |
| format_checker=self.Validator.FORMAT_CHECKER, |
| _resolver=new._resolver, |
| ) |
|
|
| self.assertEqual(new, expected) |
| self.assertNotEqual(new, original) |
|
|
| def test_evolve_with_subclass(self): |
| """ |
| Subclassing validators isn't supported public API, but some users have |
| done it, because we don't actually error entirely when it's done :/ |
| |
| We need to deprecate doing so first to help as many of these users |
| ensure they can move to supported APIs, but this test ensures that in |
| the interim, we haven't broken those users. |
| """ |
|
|
| with self.assertWarns(DeprecationWarning): |
| @define |
| class OhNo(self.Validator): |
| foo = field(factory=lambda: [1, 2, 3]) |
| _bar = field(default=37) |
|
|
| validator = OhNo({}, bar=12) |
| self.assertEqual(validator.foo, [1, 2, 3]) |
|
|
| new = validator.evolve(schema={"type": "integer"}) |
| self.assertEqual(new.foo, [1, 2, 3]) |
| self.assertEqual(new._bar, 12) |
|
|
| def test_is_type_is_true_for_valid_type(self): |
| self.assertTrue(self.Validator({}).is_type("foo", "string")) |
|
|
| def test_is_type_is_false_for_invalid_type(self): |
| self.assertFalse(self.Validator({}).is_type("foo", "array")) |
|
|
| def test_is_type_evades_bool_inheriting_from_int(self): |
| self.assertFalse(self.Validator({}).is_type(True, "integer")) |
| self.assertFalse(self.Validator({}).is_type(True, "number")) |
|
|
| def test_it_can_validate_with_decimals(self): |
| schema = {"items": {"type": "number"}} |
| Validator = validators.extend( |
| self.Validator, |
| type_checker=self.Validator.TYPE_CHECKER.redefine( |
| "number", |
| lambda checker, thing: isinstance( |
| thing, (int, float, Decimal), |
| ) and not isinstance(thing, bool), |
| ), |
| ) |
|
|
| validator = Validator(schema) |
| validator.validate([1, 1.1, Decimal(1) / Decimal(8)]) |
|
|
| invalid = ["foo", {}, [], True, None] |
| self.assertEqual( |
| [error.instance for error in validator.iter_errors(invalid)], |
| invalid, |
| ) |
|
|
| def test_it_returns_true_for_formats_it_does_not_know_about(self): |
| validator = self.Validator( |
| {"format": "carrot"}, format_checker=FormatChecker(), |
| ) |
| validator.validate("bugs") |
|
|
| def test_it_does_not_validate_formats_by_default(self): |
| validator = self.Validator({}) |
| self.assertIsNone(validator.format_checker) |
|
|
| def test_it_validates_formats_if_a_checker_is_provided(self): |
| checker = FormatChecker() |
| bad = ValueError("Bad!") |
|
|
| @checker.checks("foo", raises=ValueError) |
| def check(value): |
| if value == "good": |
| return True |
| elif value == "bad": |
| raise bad |
| else: |
| self.fail(f"What is {value}? [Baby Don't Hurt Me]") |
|
|
| validator = self.Validator( |
| {"format": "foo"}, format_checker=checker, |
| ) |
|
|
| validator.validate("good") |
| with self.assertRaises(exceptions.ValidationError) as cm: |
| validator.validate("bad") |
|
|
| |
| self.assertIs(cm.exception.cause, bad) |
|
|
| def test_non_string_custom_type(self): |
| non_string_type = object() |
| schema = {"type": [non_string_type]} |
| Crazy = validators.extend( |
| self.Validator, |
| type_checker=self.Validator.TYPE_CHECKER.redefine( |
| non_string_type, |
| lambda checker, thing: isinstance(thing, int), |
| ), |
| ) |
| Crazy(schema).validate(15) |
|
|
| def test_it_properly_formats_tuples_in_errors(self): |
| """ |
| A tuple instance properly formats validation errors for uniqueItems. |
| |
| See #224 |
| """ |
| TupleValidator = validators.extend( |
| self.Validator, |
| type_checker=self.Validator.TYPE_CHECKER.redefine( |
| "array", |
| lambda checker, thing: isinstance(thing, tuple), |
| ), |
| ) |
| with self.assertRaises(exceptions.ValidationError) as e: |
| TupleValidator({"uniqueItems": True}).validate((1, 1)) |
| self.assertIn("(1, 1) has non-unique elements", str(e.exception)) |
|
|
| def test_check_redefined_sequence(self): |
| """ |
| Allow array to validate against another defined sequence type |
| """ |
| schema = {"type": "array", "uniqueItems": True} |
| MyMapping = namedtuple("MyMapping", "a, b") |
| Validator = validators.extend( |
| self.Validator, |
| type_checker=self.Validator.TYPE_CHECKER.redefine_many( |
| { |
| "array": lambda checker, thing: isinstance( |
| thing, (list, deque), |
| ), |
| "object": lambda checker, thing: isinstance( |
| thing, (dict, MyMapping), |
| ), |
| }, |
| ), |
| ) |
| validator = Validator(schema) |
|
|
| valid_instances = [ |
| deque(["a", None, "1", "", True]), |
| deque([[False], [0]]), |
| [deque([False]), deque([0])], |
| [[deque([False])], [deque([0])]], |
| [[[[[deque([False])]]]], [[[[deque([0])]]]]], |
| [deque([deque([False])]), deque([deque([0])])], |
| [MyMapping("a", 0), MyMapping("a", False)], |
| [ |
| MyMapping("a", [deque([0])]), |
| MyMapping("a", [deque([False])]), |
| ], |
| [ |
| MyMapping("a", [MyMapping("a", deque([0]))]), |
| MyMapping("a", [MyMapping("a", deque([False]))]), |
| ], |
| [deque(deque(deque([False]))), deque(deque(deque([0])))], |
| ] |
|
|
| for instance in valid_instances: |
| validator.validate(instance) |
|
|
| invalid_instances = [ |
| deque(["a", "b", "a"]), |
| deque([[False], [False]]), |
| [deque([False]), deque([False])], |
| [[deque([False])], [deque([False])]], |
| [[[[[deque([False])]]]], [[[[deque([False])]]]]], |
| [deque([deque([False])]), deque([deque([False])])], |
| [MyMapping("a", False), MyMapping("a", False)], |
| [ |
| MyMapping("a", [deque([False])]), |
| MyMapping("a", [deque([False])]), |
| ], |
| [ |
| MyMapping("a", [MyMapping("a", deque([False]))]), |
| MyMapping("a", [MyMapping("a", deque([False]))]), |
| ], |
| [deque(deque(deque([False]))), deque(deque(deque([False])))], |
| ] |
|
|
| for instance in invalid_instances: |
| with self.assertRaises(exceptions.ValidationError): |
| validator.validate(instance) |
|
|
| def test_it_creates_a_ref_resolver_if_not_provided(self): |
| with self.assertWarns(DeprecationWarning): |
| resolver = self.Validator({}).resolver |
| self.assertIsInstance(resolver, validators._RefResolver) |
|
|
| def test_it_upconverts_from_deprecated_RefResolvers(self): |
| ref, schema = "someCoolRef", {"type": "integer"} |
| resolver = validators._RefResolver("", {}, store={ref: schema}) |
| validator = self.Validator({"$ref": ref}, resolver=resolver) |
|
|
| with self.assertRaises(exceptions.ValidationError): |
| validator.validate(None) |
|
|
| def test_it_upconverts_from_yet_older_deprecated_legacy_RefResolvers(self): |
| """ |
| Legacy RefResolvers support only the context manager form of |
| resolution. |
| """ |
|
|
| class LegacyRefResolver: |
| @contextmanager |
| def resolving(this, ref): |
| self.assertEqual(ref, "the ref") |
| yield {"type": "integer"} |
|
|
| resolver = LegacyRefResolver() |
| schema = {"$ref": "the ref"} |
|
|
| with self.assertRaises(exceptions.ValidationError): |
| self.Validator(schema, resolver=resolver).validate(None) |
|
|
|
|
| class AntiDraft6LeakMixin: |
| """ |
| Make sure functionality from draft 6 doesn't leak backwards in time. |
| """ |
|
|
| def test_True_is_not_a_schema(self): |
| with self.assertRaises(exceptions.SchemaError) as e: |
| self.Validator.check_schema(True) |
| self.assertIn("True is not of type", str(e.exception)) |
|
|
| def test_False_is_not_a_schema(self): |
| with self.assertRaises(exceptions.SchemaError) as e: |
| self.Validator.check_schema(False) |
| self.assertIn("False is not of type", str(e.exception)) |
|
|
| def test_True_is_not_a_schema_even_if_you_forget_to_check(self): |
| with self.assertRaises(Exception) as e: |
| self.Validator(True).validate(12) |
| self.assertNotIsInstance(e.exception, exceptions.ValidationError) |
|
|
| def test_False_is_not_a_schema_even_if_you_forget_to_check(self): |
| with self.assertRaises(Exception) as e: |
| self.Validator(False).validate(12) |
| self.assertNotIsInstance(e.exception, exceptions.ValidationError) |
|
|
|
|
| class TestDraft3Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase): |
| Validator = validators.Draft3Validator |
| valid: tuple[dict, dict] = ({}, {}) |
| invalid = {"type": "integer"}, "foo" |
|
|
| def test_any_type_is_valid_for_type_any(self): |
| validator = self.Validator({"type": "any"}) |
| validator.validate(object()) |
|
|
| def test_any_type_is_redefinable(self): |
| """ |
| Sigh, because why not. |
| """ |
| Crazy = validators.extend( |
| self.Validator, |
| type_checker=self.Validator.TYPE_CHECKER.redefine( |
| "any", lambda checker, thing: isinstance(thing, int), |
| ), |
| ) |
| validator = Crazy({"type": "any"}) |
| validator.validate(12) |
| with self.assertRaises(exceptions.ValidationError): |
| validator.validate("foo") |
|
|
| def test_is_type_is_true_for_any_type(self): |
| self.assertTrue(self.Validator({"type": "any"}).is_valid(object())) |
|
|
| def test_is_type_does_not_evade_bool_if_it_is_being_tested(self): |
| self.assertTrue(self.Validator({}).is_type(True, "boolean")) |
| self.assertTrue(self.Validator({"type": "any"}).is_valid(True)) |
|
|
|
|
| class TestDraft4Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase): |
| Validator = validators.Draft4Validator |
| valid: tuple[dict, dict] = ({}, {}) |
| invalid = {"type": "integer"}, "foo" |
|
|
|
|
| class TestDraft6Validator(ValidatorTestMixin, TestCase): |
| Validator = validators.Draft6Validator |
| valid: tuple[dict, dict] = ({}, {}) |
| invalid = {"type": "integer"}, "foo" |
|
|
|
|
| class TestDraft7Validator(ValidatorTestMixin, TestCase): |
| Validator = validators.Draft7Validator |
| valid: tuple[dict, dict] = ({}, {}) |
| invalid = {"type": "integer"}, "foo" |
|
|
|
|
| class TestDraft201909Validator(ValidatorTestMixin, TestCase): |
| Validator = validators.Draft201909Validator |
| valid: tuple[dict, dict] = ({}, {}) |
| invalid = {"type": "integer"}, "foo" |
|
|
|
|
| class TestDraft202012Validator(ValidatorTestMixin, TestCase): |
| Validator = validators.Draft202012Validator |
| valid: tuple[dict, dict] = ({}, {}) |
| invalid = {"type": "integer"}, "foo" |
|
|
|
|
| class TestLatestValidator(TestCase): |
| """ |
| These really apply to multiple versions but are easiest to test on one. |
| """ |
|
|
| def test_ref_resolvers_may_have_boolean_schemas_stored(self): |
| ref = "someCoolRef" |
| schema = {"$ref": ref} |
| resolver = validators._RefResolver("", {}, store={ref: False}) |
| validator = validators._LATEST_VERSION(schema, resolver=resolver) |
|
|
| with self.assertRaises(exceptions.ValidationError): |
| validator.validate(None) |
|
|
|
|
| class TestValidatorFor(TestCase): |
| def test_draft_3(self): |
| schema = {"$schema": "http://json-schema.org/draft-03/schema"} |
| self.assertIs( |
| validators.validator_for(schema), |
| validators.Draft3Validator, |
| ) |
|
|
| schema = {"$schema": "http://json-schema.org/draft-03/schema#"} |
| self.assertIs( |
| validators.validator_for(schema), |
| validators.Draft3Validator, |
| ) |
|
|
| def test_draft_4(self): |
| schema = {"$schema": "http://json-schema.org/draft-04/schema"} |
| self.assertIs( |
| validators.validator_for(schema), |
| validators.Draft4Validator, |
| ) |
|
|
| schema = {"$schema": "http://json-schema.org/draft-04/schema#"} |
| self.assertIs( |
| validators.validator_for(schema), |
| validators.Draft4Validator, |
| ) |
|
|
| def test_draft_6(self): |
| schema = {"$schema": "http://json-schema.org/draft-06/schema"} |
| self.assertIs( |
| validators.validator_for(schema), |
| validators.Draft6Validator, |
| ) |
|
|
| schema = {"$schema": "http://json-schema.org/draft-06/schema#"} |
| self.assertIs( |
| validators.validator_for(schema), |
| validators.Draft6Validator, |
| ) |
|
|
| def test_draft_7(self): |
| schema = {"$schema": "http://json-schema.org/draft-07/schema"} |
| self.assertIs( |
| validators.validator_for(schema), |
| validators.Draft7Validator, |
| ) |
|
|
| schema = {"$schema": "http://json-schema.org/draft-07/schema#"} |
| self.assertIs( |
| validators.validator_for(schema), |
| validators.Draft7Validator, |
| ) |
|
|
| def test_draft_201909(self): |
| schema = {"$schema": "https://json-schema.org/draft/2019-09/schema"} |
| self.assertIs( |
| validators.validator_for(schema), |
| validators.Draft201909Validator, |
| ) |
|
|
| schema = {"$schema": "https://json-schema.org/draft/2019-09/schema#"} |
| self.assertIs( |
| validators.validator_for(schema), |
| validators.Draft201909Validator, |
| ) |
|
|
| def test_draft_202012(self): |
| schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"} |
| self.assertIs( |
| validators.validator_for(schema), |
| validators.Draft202012Validator, |
| ) |
|
|
| schema = {"$schema": "https://json-schema.org/draft/2020-12/schema#"} |
| self.assertIs( |
| validators.validator_for(schema), |
| validators.Draft202012Validator, |
| ) |
|
|
| def test_True(self): |
| self.assertIs( |
| validators.validator_for(True), |
| validators._LATEST_VERSION, |
| ) |
|
|
| def test_False(self): |
| self.assertIs( |
| validators.validator_for(False), |
| validators._LATEST_VERSION, |
| ) |
|
|
| def test_custom_validator(self): |
| Validator = validators.create( |
| meta_schema={"id": "meta schema id"}, |
| version="12", |
| id_of=lambda s: s.get("id", ""), |
| ) |
| schema = {"$schema": "meta schema id"} |
| self.assertIs( |
| validators.validator_for(schema), |
| Validator, |
| ) |
|
|
| def test_custom_validator_draft6(self): |
| Validator = validators.create( |
| meta_schema={"$id": "meta schema $id"}, |
| version="13", |
| ) |
| schema = {"$schema": "meta schema $id"} |
| self.assertIs( |
| validators.validator_for(schema), |
| Validator, |
| ) |
|
|
| def test_validator_for_jsonschema_default(self): |
| self.assertIs(validators.validator_for({}), validators._LATEST_VERSION) |
|
|
| def test_validator_for_custom_default(self): |
| self.assertIs(validators.validator_for({}, default=None), None) |
|
|
| def test_warns_if_meta_schema_specified_was_not_found(self): |
| with self.assertWarns(DeprecationWarning) as cm: |
| validators.validator_for(schema={"$schema": "unknownSchema"}) |
|
|
| self.assertEqual(cm.filename, __file__) |
| self.assertEqual( |
| str(cm.warning), |
| "The metaschema specified by $schema was not found. " |
| "Using the latest draft to validate, but this will raise " |
| "an error in the future.", |
| ) |
|
|
| def test_does_not_warn_if_meta_schema_is_unspecified(self): |
| with warnings.catch_warnings(record=True) as w: |
| warnings.simplefilter("always") |
| validators.validator_for(schema={}, default={}) |
| self.assertFalse(w) |
|
|
| def test_validator_for_custom_default_with_schema(self): |
| schema, default = {"$schema": "mailto:foo@example.com"}, object() |
| self.assertIs(validators.validator_for(schema, default), default) |
|
|
|
|
| class TestValidate(TestCase): |
| def assertUses(self, schema, Validator): |
| result = [] |
| with mock.patch.object(Validator, "check_schema", result.append): |
| validators.validate({}, schema) |
| self.assertEqual(result, [schema]) |
|
|
| def test_draft3_validator_is_chosen(self): |
| self.assertUses( |
| schema={"$schema": "http://json-schema.org/draft-03/schema#"}, |
| Validator=validators.Draft3Validator, |
| ) |
| |
| self.assertUses( |
| schema={"$schema": "http://json-schema.org/draft-03/schema"}, |
| Validator=validators.Draft3Validator, |
| ) |
|
|
| def test_draft4_validator_is_chosen(self): |
| self.assertUses( |
| schema={"$schema": "http://json-schema.org/draft-04/schema#"}, |
| Validator=validators.Draft4Validator, |
| ) |
| |
| self.assertUses( |
| schema={"$schema": "http://json-schema.org/draft-04/schema"}, |
| Validator=validators.Draft4Validator, |
| ) |
|
|
| def test_draft6_validator_is_chosen(self): |
| self.assertUses( |
| schema={"$schema": "http://json-schema.org/draft-06/schema#"}, |
| Validator=validators.Draft6Validator, |
| ) |
| |
| self.assertUses( |
| schema={"$schema": "http://json-schema.org/draft-06/schema"}, |
| Validator=validators.Draft6Validator, |
| ) |
|
|
| def test_draft7_validator_is_chosen(self): |
| self.assertUses( |
| schema={"$schema": "http://json-schema.org/draft-07/schema#"}, |
| Validator=validators.Draft7Validator, |
| ) |
| |
| self.assertUses( |
| schema={"$schema": "http://json-schema.org/draft-07/schema"}, |
| Validator=validators.Draft7Validator, |
| ) |
|
|
| def test_draft202012_validator_is_chosen(self): |
| self.assertUses( |
| schema={ |
| "$schema": "https://json-schema.org/draft/2020-12/schema#", |
| }, |
| Validator=validators.Draft202012Validator, |
| ) |
| |
| self.assertUses( |
| schema={ |
| "$schema": "https://json-schema.org/draft/2020-12/schema", |
| }, |
| Validator=validators.Draft202012Validator, |
| ) |
|
|
| def test_draft202012_validator_is_the_default(self): |
| self.assertUses(schema={}, Validator=validators.Draft202012Validator) |
|
|
| def test_validation_error_message(self): |
| with self.assertRaises(exceptions.ValidationError) as e: |
| validators.validate(12, {"type": "string"}) |
| self.assertRegex( |
| str(e.exception), |
| "(?s)Failed validating '.*' in schema.*On instance", |
| ) |
|
|
| def test_schema_error_message(self): |
| with self.assertRaises(exceptions.SchemaError) as e: |
| validators.validate(12, {"type": 12}) |
| self.assertRegex( |
| str(e.exception), |
| "(?s)Failed validating '.*' in metaschema.*On schema", |
| ) |
|
|
| def test_it_uses_best_match(self): |
| schema = { |
| "oneOf": [ |
| {"type": "number", "minimum": 20}, |
| {"type": "array"}, |
| ], |
| } |
| with self.assertRaises(exceptions.ValidationError) as e: |
| validators.validate(12, schema) |
| self.assertIn("12 is less than the minimum of 20", str(e.exception)) |
|
|
|
|
| class TestThreading(TestCase): |
| """ |
| Threading-related functionality tests. |
| |
| jsonschema doesn't promise thread safety, and its validation behavior |
| across multiple threads may change at any time, but that means it isn't |
| safe to share *validators* across threads, not that anytime one has |
| multiple threads that jsonschema won't work (it certainly is intended to). |
| |
| These tests ensure that this minimal level of functionality continues to |
| work. |
| """ |
|
|
| def test_validation_across_a_second_thread(self): |
| failed = [] |
|
|
| def validate(): |
| try: |
| validators.validate(instance=37, schema=True) |
| except: |
| failed.append(sys.exc_info()) |
|
|
| validate() |
|
|
| from threading import Thread |
| thread = Thread(target=validate) |
| thread.start() |
| thread.join() |
| self.assertEqual((thread.is_alive(), failed), (False, [])) |
|
|
|
|
| class TestReferencing(TestCase): |
| def test_registry_with_retrieve(self): |
| def retrieve(uri): |
| return DRAFT202012.create_resource({"type": "integer"}) |
|
|
| registry = referencing.Registry(retrieve=retrieve) |
| schema = {"$ref": "https://example.com/"} |
| validator = validators.Draft202012Validator(schema, registry=registry) |
|
|
| self.assertEqual( |
| (validator.is_valid(12), validator.is_valid("foo")), |
| (True, False), |
| ) |
|
|
| def test_custom_registries_do_not_autoretrieve_remote_resources(self): |
| registry = referencing.Registry() |
| schema = {"$ref": "https://example.com/"} |
| validator = validators.Draft202012Validator(schema, registry=registry) |
|
|
| with warnings.catch_warnings(record=True) as w: |
| warnings.simplefilter("always") |
| with self.assertRaises(referencing.exceptions.Unresolvable): |
| validator.validate(12) |
| self.assertFalse(w) |
|
|
|
|
| class TestRefResolver(TestCase): |
|
|
| base_uri = "" |
| stored_uri = "foo://stored" |
| stored_schema = {"stored": "schema"} |
|
|
| def setUp(self): |
| self.referrer = {} |
| self.store = {self.stored_uri: self.stored_schema} |
| self.resolver = validators._RefResolver( |
| self.base_uri, self.referrer, self.store, |
| ) |
|
|
| def test_it_does_not_retrieve_schema_urls_from_the_network(self): |
| ref = validators.Draft3Validator.META_SCHEMA["id"] |
| with mock.patch.object(self.resolver, "resolve_remote") as patched: |
| with self.resolver.resolving(ref) as resolved: |
| pass |
| self.assertEqual(resolved, validators.Draft3Validator.META_SCHEMA) |
| self.assertFalse(patched.called) |
|
|
| def test_it_resolves_local_refs(self): |
| ref = "#/properties/foo" |
| self.referrer["properties"] = {"foo": object()} |
| with self.resolver.resolving(ref) as resolved: |
| self.assertEqual(resolved, self.referrer["properties"]["foo"]) |
|
|
| def test_it_resolves_local_refs_with_id(self): |
| schema = {"id": "http://bar/schema#", "a": {"foo": "bar"}} |
| resolver = validators._RefResolver.from_schema( |
| schema, |
| id_of=lambda schema: schema.get("id", ""), |
| ) |
| with resolver.resolving("#/a") as resolved: |
| self.assertEqual(resolved, schema["a"]) |
| with resolver.resolving("http://bar/schema#/a") as resolved: |
| self.assertEqual(resolved, schema["a"]) |
|
|
| def test_it_retrieves_stored_refs(self): |
| with self.resolver.resolving(self.stored_uri) as resolved: |
| self.assertIs(resolved, self.stored_schema) |
|
|
| self.resolver.store["cached_ref"] = {"foo": 12} |
| with self.resolver.resolving("cached_ref#/foo") as resolved: |
| self.assertEqual(resolved, 12) |
|
|
| def test_it_retrieves_unstored_refs_via_requests(self): |
| ref = "http://bar#baz" |
| schema = {"baz": 12} |
|
|
| if "requests" in sys.modules: |
| self.addCleanup( |
| sys.modules.__setitem__, "requests", sys.modules["requests"], |
| ) |
| sys.modules["requests"] = ReallyFakeRequests({"http://bar": schema}) |
|
|
| with self.resolver.resolving(ref) as resolved: |
| self.assertEqual(resolved, 12) |
|
|
| def test_it_retrieves_unstored_refs_via_urlopen(self): |
| ref = "http://bar#baz" |
| schema = {"baz": 12} |
|
|
| if "requests" in sys.modules: |
| self.addCleanup( |
| sys.modules.__setitem__, "requests", sys.modules["requests"], |
| ) |
| sys.modules["requests"] = None |
|
|
| @contextmanager |
| def fake_urlopen(url): |
| self.assertEqual(url, "http://bar") |
| yield BytesIO(json.dumps(schema).encode("utf8")) |
|
|
| self.addCleanup(setattr, validators, "urlopen", validators.urlopen) |
| validators.urlopen = fake_urlopen |
|
|
| with self.resolver.resolving(ref) as resolved: |
| pass |
| self.assertEqual(resolved, 12) |
|
|
| def test_it_retrieves_local_refs_via_urlopen(self): |
| with tempfile.NamedTemporaryFile(delete=False, mode="wt") as tempf: |
| self.addCleanup(os.remove, tempf.name) |
| json.dump({"foo": "bar"}, tempf) |
|
|
| ref = f"file://{pathname2url(tempf.name)}#foo" |
| with self.resolver.resolving(ref) as resolved: |
| self.assertEqual(resolved, "bar") |
|
|
| def test_it_can_construct_a_base_uri_from_a_schema(self): |
| schema = {"id": "foo"} |
| resolver = validators._RefResolver.from_schema( |
| schema, |
| id_of=lambda schema: schema.get("id", ""), |
| ) |
| self.assertEqual(resolver.base_uri, "foo") |
| self.assertEqual(resolver.resolution_scope, "foo") |
| with resolver.resolving("") as resolved: |
| self.assertEqual(resolved, schema) |
| with resolver.resolving("#") as resolved: |
| self.assertEqual(resolved, schema) |
| with resolver.resolving("foo") as resolved: |
| self.assertEqual(resolved, schema) |
| with resolver.resolving("foo#") as resolved: |
| self.assertEqual(resolved, schema) |
|
|
| def test_it_can_construct_a_base_uri_from_a_schema_without_id(self): |
| schema = {} |
| resolver = validators._RefResolver.from_schema(schema) |
| self.assertEqual(resolver.base_uri, "") |
| self.assertEqual(resolver.resolution_scope, "") |
| with resolver.resolving("") as resolved: |
| self.assertEqual(resolved, schema) |
| with resolver.resolving("#") as resolved: |
| self.assertEqual(resolved, schema) |
|
|
| def test_custom_uri_scheme_handlers(self): |
| def handler(url): |
| self.assertEqual(url, ref) |
| return schema |
|
|
| schema = {"foo": "bar"} |
| ref = "foo://bar" |
| resolver = validators._RefResolver("", {}, handlers={"foo": handler}) |
| with resolver.resolving(ref) as resolved: |
| self.assertEqual(resolved, schema) |
|
|
| def test_cache_remote_on(self): |
| response = [object()] |
|
|
| def handler(url): |
| try: |
| return response.pop() |
| except IndexError: |
| self.fail("Response must not have been cached!") |
|
|
| ref = "foo://bar" |
| resolver = validators._RefResolver( |
| "", {}, cache_remote=True, handlers={"foo": handler}, |
| ) |
| with resolver.resolving(ref): |
| pass |
| with resolver.resolving(ref): |
| pass |
|
|
| def test_cache_remote_off(self): |
| response = [object()] |
|
|
| def handler(url): |
| try: |
| return response.pop() |
| except IndexError: |
| self.fail("Handler called twice!") |
|
|
| ref = "foo://bar" |
| resolver = validators._RefResolver( |
| "", {}, cache_remote=False, handlers={"foo": handler}, |
| ) |
| with resolver.resolving(ref): |
| pass |
|
|
| def test_if_you_give_it_junk_you_get_a_resolution_error(self): |
| error = ValueError("Oh no! What's this?") |
|
|
| def handler(url): |
| raise error |
|
|
| ref = "foo://bar" |
| resolver = validators._RefResolver("", {}, handlers={"foo": handler}) |
| with self.assertRaises(exceptions._RefResolutionError) as err: |
| with resolver.resolving(ref): |
| self.fail("Shouldn't get this far!") |
| self.assertEqual(err.exception, exceptions._RefResolutionError(error)) |
|
|
| def test_helpful_error_message_on_failed_pop_scope(self): |
| resolver = validators._RefResolver("", {}) |
| resolver.pop_scope() |
| with self.assertRaises(exceptions._RefResolutionError) as exc: |
| resolver.pop_scope() |
| self.assertIn("Failed to pop the scope", str(exc.exception)) |
|
|
| def test_pointer_within_schema_with_different_id(self): |
| """ |
| See #1085. |
| """ |
| schema = validators.Draft7Validator.META_SCHEMA |
| one = validators._RefResolver("", schema) |
| validator = validators.Draft7Validator(schema, resolver=one) |
| self.assertFalse(validator.is_valid({"maxLength": "foo"})) |
|
|
| another = { |
| "allOf": [{"$ref": validators.Draft7Validator.META_SCHEMA["$id"]}], |
| } |
| two = validators._RefResolver("", another) |
| validator = validators.Draft7Validator(another, resolver=two) |
| self.assertFalse(validator.is_valid({"maxLength": "foo"})) |
|
|
| def test_newly_created_validator_with_ref_resolver(self): |
| """ |
| See https://github.com/python-jsonschema/jsonschema/issues/1061#issuecomment-1624266555. |
| """ |
|
|
| def handle(uri): |
| self.assertEqual(uri, "http://example.com/foo") |
| return {"type": "integer"} |
|
|
| resolver = validators._RefResolver("", {}, handlers={"http": handle}) |
| Validator = validators.create( |
| meta_schema={}, |
| validators=validators.Draft4Validator.VALIDATORS, |
| ) |
| schema = {"$id": "http://example.com/bar", "$ref": "foo"} |
| validator = Validator(schema, resolver=resolver) |
| self.assertEqual( |
| (validator.is_valid({}), validator.is_valid(37)), |
| (False, True), |
| ) |
|
|
| def test_refresolver_with_pointer_in_schema_with_no_id(self): |
| """ |
| See https://github.com/python-jsonschema/jsonschema/issues/1124#issuecomment-1632574249. |
| """ |
|
|
| schema = { |
| "properties": {"x": {"$ref": "#/definitions/x"}}, |
| "definitions": {"x": {"type": "integer"}}, |
| } |
|
|
| validator = validators.Draft202012Validator( |
| schema, |
| resolver=validators._RefResolver("", schema), |
| ) |
| self.assertEqual( |
| (validator.is_valid({"x": "y"}), validator.is_valid({"x": 37})), |
| (False, True), |
| ) |
|
|
|
|
|
|
| def sorted_errors(errors): |
| def key(error): |
| return ( |
| [str(e) for e in error.path], |
| [str(e) for e in error.schema_path], |
| ) |
| return sorted(errors, key=key) |
|
|
|
|
| @define |
| class ReallyFakeRequests: |
|
|
| _responses: dict[str, Any] |
|
|
| def get(self, url): |
| response = self._responses.get(url) |
| if url is None: |
| raise ValueError("Unknown URL: " + repr(url)) |
| return _ReallyFakeJSONResponse(json.dumps(response)) |
|
|
|
|
| @define |
| class _ReallyFakeJSONResponse: |
|
|
| _response: str |
|
|
| def json(self): |
| return json.loads(self._response) |
|
|