Permalink
0504605 Feb 8, 2016
@zyga @jayvdb @codehelp
783 lines (764 sloc) 27.4 KB
# Copyright (C) 2010, 2011 Linaro Limited
# Copyright (C) 2016 Zygmunt Krynicki
#
# Author: Zygmunt Krynicki <me@zygoon.pl>
#
# This file is part of json-schema-validator.
#
# json-schema-validator is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3
# as published by the Free Software Foundation
#
# json-schema-validator is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with json-schema-validator. If not, see <http://www.gnu.org/licenses/>.
"""
Unit tests for JSON schema
"""
import functools
import json
import sys
from testscenarios import TestWithScenarios
from testtools import TestCase
from json_schema_validator.errors import ValidationError
from json_schema_validator.shortcuts import validate
from json_schema_validator.validator import Validator
PY2 = sys.version_info[0] == 2
if PY2:
import yaml
def deserializer(json_string):
# Always check the serialised JSON using the native JSON loader
# so that any error messages are consistent and appropriate.
json.loads(json_string)
return yaml.safe_load(json_string)
validate = functools.partial(validate, deserializer=deserializer)
class ValidatorFailureTests(TestWithScenarios, TestCase):
scenarios = [
("type_string_got_null", {
'schema': '{"type": "string"}',
'data': 'null',
'raises': ValidationError(
"None does not match type 'string'",
"Object has incorrect type (expected string)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_string_got_integer", {
'schema': '{"type": "string"}',
'data': '5',
'raises': ValidationError(
"5 does not match type 'string'",
"Object has incorrect type (expected string)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_number_got_null", {
'schema': '{"type": "number"}',
'data': 'null',
'raises': ValidationError(
"None does not match type 'number'",
"Object has incorrect type (expected number)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_number_got_string", {
'schema': '{"type": "number"}',
'data': '"foobar"',
'raises': ValidationError(
"'foobar' does not match type 'number'",
"Object has incorrect type (expected number)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_number_got_string_that_looks_like_number", {
'schema': '{"type": "number"}',
'data': '"3"',
'raises': ValidationError(
"'3' does not match type 'number'",
"Object has incorrect type (expected number)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_integer_got_float", {
'schema': '{"type": "integer"}',
'data': '1.5',
'raises': ValidationError(
"1.5 does not match type 'integer'",
"Object has incorrect type (expected integer)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_integer_got_null", {
'schema': '{"type": "integer"}',
'data': 'null',
'raises': ValidationError(
"None does not match type 'integer'",
"Object has incorrect type (expected integer)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_boolean_got_null", {
'schema': '{"type": "boolean"}',
'data': 'null',
'raises': ValidationError(
"None does not match type 'boolean'",
"Object has incorrect type (expected boolean)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_boolean_got_empty_string", {
'schema': '{"type": "boolean"}',
'data': '""',
'raises': ValidationError(
"'' does not match type 'boolean'",
"Object has incorrect type (expected boolean)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_boolean_got_empty_list", {
'schema': '{"type": "boolean"}',
'data': '[]',
'raises': ValidationError(
"[] does not match type 'boolean'",
"Object has incorrect type (expected boolean)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_boolean_got_empty_object", {
'schema': '{"type": "boolean"}',
'data': '{}',
'raises': ValidationError(
"{} does not match type 'boolean'",
"Object has incorrect type (expected boolean)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_object_got_integer", {
'schema': '{"type": "object"}',
'data': '1',
'raises': ValidationError(
"1 does not match type 'object'",
"Object has incorrect type (expected object)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_object_got_null", {
'schema': '{"type": "object"}',
'data': 'null',
'raises': ValidationError(
"None does not match type 'object'",
"Object has incorrect type (expected object)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_array_got_null", {
'schema': '{"type": "array"}',
'data': 'null',
'raises': ValidationError(
"None does not match type 'array'",
"Object has incorrect type (expected array)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_array_got_integer", {
'schema': '{"type": "array"}',
'data': '1',
'raises': ValidationError(
"1 does not match type 'array'",
"Object has incorrect type (expected array)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_null_got_empty_string", {
'schema': '{"type": "null"}',
'data': '""',
'raises': ValidationError(
"'' does not match type 'null'",
"Object has incorrect type (expected null)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_null_got_zero", {
'schema': '{"type": "null"}',
'data': '0',
'raises': ValidationError(
"0 does not match type 'null'",
"Object has incorrect type (expected null)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_null_got_empty_list", {
'schema': '{"type": "null"}',
'data': '[]',
'raises': ValidationError(
"[] does not match type 'null'",
"Object has incorrect type (expected null)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_null_got_empty_object", {
'schema': '{"type": "null"}',
'data': '{}',
'raises': ValidationError(
"{} does not match type 'null'",
"Object has incorrect type (expected null)"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("type_list_got_mismatching_item", {
'schema': '{"type": ["string", "number"]}',
'data': '{}',
'raises': ValidationError(
"{} does not match any of the types in ['string', 'number']",
"Object has incorrect type (multiple types possible)",
".type"),
'object_expr': 'object',
'schema_expr': 'schema.type',
}),
("property_check_is_not_primitive", {
'schema': """
{
"type": "object",
"properties": {
"foo": {
"type": "number"
}
}
}""",
'data': '{"foo": "foobar"}',
'raises': ValidationError(
"'foobar' does not match type 'number'",
"Object has incorrect type (expected number)"),
'object_expr': 'object.foo',
'schema_expr': 'schema.properties.foo.type',
}),
("property_check_validates_optional_properties", {
'schema': """
{
"type": "object",
"properties": {
"foo": {
"type": "number",
"optional": true
}
}
}""",
'data': '{"foo": null}',
'raises': ValidationError(
"None does not match type 'number'",
"Object has incorrect type (expected number)"),
'object_expr': 'object.foo',
'schema_expr': 'schema.properties.foo.type',
}),
("property_check_reports_missing_non_optional_properties", {
'schema': """
{
"type": "object",
"properties": {
"foo": {
"type": "number",
"optional": false
}
}
}""",
'data': '{}',
'raises': ValidationError(
"{} does not have property 'foo'",
"Object lacks property 'foo'"),
'object_expr': 'object',
'schema_expr': 'schema.properties.foo.optional',
}),
("property_check_reports_unknown_properties_when_additionalProperties_is_false", {
'schema': """
{
"type": "object",
"additionalProperties": false
}""",
'data': '{"foo": 5}',
'raises': ValidationError(
"{'foo': 5} has unknown property 'foo' and"
" additionalProperties is false",
"Object has unknown property 'foo' but additional "
"properties are disallowed"),
'object_expr': 'object',
'schema_expr': 'schema.additionalProperties',
}),
("property_check_validates_additional_properties_using_specified_type_when_additionalProperties_is_an_object_violation", {
'schema': """
{
"type": "object",
"additionalProperties": {
"type": "string"
}
}""",
'data': '{"foo": "aaa", "bar": 5}',
'raises': ValidationError(
"5 does not match type 'string'",
"Object has incorrect type (expected string)"),
'object_expr': 'object.bar',
'schema_expr': 'schema.additionalProperties.type',
}),
("enum_check_reports_unlisted_values", {
'schema': '{"enum": [1, 2, 3]}',
'data': '5',
'raises': ValidationError(
'5 does not match any value in enumeration [1, 2, 3]',
"Object does not match any value in enumeration"),
'object_expr': 'object',
'schema_expr': 'schema.enum',
}),
("items_with_single_schema_finds_problems", {
'schema': '{"items": {"type": "string"}}',
'data': '["foo", null, "froz"]',
'raises': ValidationError(
"None does not match type 'string'",
"Object has incorrect type (expected string)"),
'object_expr': 'object[1]',
'schema_expr': 'schema.items.type',
}),
("items_with_array_schema_checks_for_too_short_data", {
'schema': """
{
"items": [
{"type": "string"},
{"type": "boolean"}
]
}""",
'data': '["foo"]',
'raises': ValidationError(
"['foo'] is shorter than array schema [{'type':"
" 'string'}, {'type': 'boolean'}]",
"Object array is shorter than schema array"),
'object_expr': 'object',
'schema_expr': 'schema.items',
}),
("items_with_array_schema_and_additionalProperties_of_false_checks_for_too_much_data", {
'schema': """
{
"items": [
{"type": "string"},
{"type": "boolean"}
],
"additionalProperties": false
}""",
'data': '["foo", false, 5]',
'raises': ValidationError(
"['foo', False, 5] is not of the same length as array"
" schema [{'type': 'string'}, {'type': 'boolean'}] and"
" additionalProperties is false",
"Object array is not of the same length as schema array"),
'object_expr': 'object',
'schema_expr': 'schema.items',
}),
("items_with_array_schema_and_additionalProperties_can_find_problems", {
'schema': """
{
"items": [
{"type": "string"},
{"type": "boolean"}
],
"additionalProperties": {
"type": "number"
}
}""",
'data': '["foo", false, 5, 7.9, null]',
'raises': ValidationError(
"None does not match type 'number'",
"Object has incorrect type (expected number)"),
'object_expr': 'object[4]',
'schema_expr': 'schema.additionalProperties.type',
}),
("array_with_array_schema_and_uniqueItems_is_True", {
'schema': """
{
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
}""",
'data': '["foo", "bar", "foo"]',
'raises': ValidationError(
"Repeated items found in ['foo', 'bar', 'foo']",
"Repeated items found in array"),
'object_expr': 'object',
'schema_expr': 'schema.items',
}),
("requires_with_simple_property_name_can_report_problems", {
'schema': """
{
"properties": {
"foo": {
"optional": true
},
"bar": {
"requires": "foo",
"optional": true
}
}
}
""",
'data': '{"bar": null}',
'raises': ValidationError(
"None requires presence of property 'foo' in the same"
" object",
"Enclosing object does not have property 'foo'"),
'object_expr': 'object.bar',
'schema_expr': 'schema.properties.bar.requires',
}),
("requires_with_simple_property_name_can_report_problems_while_nested", {
'schema': """
{
"type": "object",
"properties": {
"nested": {
"properties": {
"foo": {
"optional": true
},
"bar": {
"requires": "foo",
"optional": true
}
}
}
}
}
""",
'data': '{"nested": {"bar": null}}',
'raises': ValidationError(
"None requires presence of property 'foo' in the same"
" object",
"Enclosing object does not have property 'foo'"),
'object_expr': 'object.nested.bar',
'schema_expr': 'schema.properties.nested.properties.bar.requires',
}),
("requires_with_schema_can_report_problems", {
'schema': """
{
"properties": {
"foo": {
"optional": true
},
"bar": {
"requires": {
"properties": {
"foo": {
"type": "number"
}
}
},
"optional": true
}
}
}
""",
'data': '{"bar": null}',
'raises': ValidationError(
"{'bar': None} does not have property 'foo'",
"Object lacks property 'foo'"),
'object_expr': 'object',
'schema_expr': 'schema.properties.bar.requires.properties.foo.optional',
}),
("requires_with_schema_can_report_subtle_problems", {
# In this test presence of "bar" requires that "foo" is
# present and has a type "number"
'schema': """
{
"properties": {
"foo": {
"optional": true
},
"bar": {
"requires": {
"properties": {
"foo": {
"type": "number"
}
}
},
"optional": true
}
}
}
""",
'data': '{"bar": null, "foo": "not a number"}',
'raises': ValidationError(
"'not a number' does not match type 'number'",
"Object has incorrect type (expected number)"),
'object_expr': 'object.foo',
'schema_expr': 'schema.properties.bar.requires.properties.foo.type'
}),
("format_date_time_finds_problems", {
'schema': '{"format": "date-time"}',
'data': '"broken"',
'raises': ValidationError(
"'broken' is not a string representing JSON date-time",
"Object is not a string representing JSON date-time"),
'object_expr': 'object',
'schema_expr': 'schema.format'
}),
]
def test_validation_error_has_proper_message(self):
ex = self.assertRaises(ValidationError,
validate, self.schema, self.data)
self.assertEqual(ex.message, self.raises.message)
def test_validation_error_has_proper_new_message(self):
ex = self.assertRaises(ValidationError,
validate, self.schema, self.data)
self.assertEqual(ex.new_message, self.raises.new_message)
def test_validation_error_has_proper_object_expr(self):
ex = self.assertRaises(ValidationError,
validate, self.schema, self.data)
self.assertEqual(ex.object_expr, self.object_expr)
def test_validation_error_has_proper_schema_expr(self):
ex = self.assertRaises(ValidationError,
validate, self.schema, self.data)
self.assertEqual(ex.schema_expr, self.schema_expr)
class ValidatorSuccessTests(TestWithScenarios, TestCase):
scenarios = [
("type_string_got_string", {
'schema': '{"type": "string"}',
'data': '"foobar"'
}),
("type_number_got_integer", {
'schema': '{"type": "number"}',
'data': '1',
}),
("type_number_number_float", {
'schema': '{"type": "number"}',
'data': '1.1',
}),
("type_integer_got_integer_one", {
'schema': '{"type": "integer"}',
'data': '1'
}),
("type_integer_got_integer", {
'schema': '{"type": "integer"}',
'data': '5'
}),
("type_boolean_got_true", {
'schema': '{"type": "boolean"}',
'data': 'true',
}),
("type_boolean_got_false", {
'schema': '{"type": "boolean"}',
'data': 'true',
}),
("type_object_got_object", {
'schema': '{"type": "object"}',
'data': '{}'
}),
("type_array_got_array", {
'schema': '{"type": "array"}',
'data': '[]'
}),
("type_null_got_null", {
'schema': '{"type": "null"}',
'data': 'null',
}),
("type_any_got_null", {
'schema': '{"type": "any"}',
'data': 'null',
}),
("type_any_got_integer", {
'schema': '{"type": "any"}',
'data': '5',
}),
("type_any_got_boolean", {
'schema': '{"type": "any"}',
'data': 'false',
}),
("type_any_got_string", {
'schema': '{"type": "any"}',
'data': '"foobar"',
}),
("type_any_got_array", {
'schema': '{"type": "any"}',
'data': '[]',
}),
("type_any_got_object", {
'schema': '{"type": "any"}',
'data': '{}',
}),
("type_nested_schema_check", {
'schema': '{"type": {"type": "number"}}',
'data': '5',
}),
("type_list_with_two_values_got_first_value", {
'schema': '{"type": ["number", "string"]}',
'data': '1',
}),
("type_list_with_two_values_got_second_value", {
'schema': '{"type": ["number", "string"]}',
'data': '"string"',
}),
("property_ignored_on_non_objects", {
'schema': '{"properties": {"foo": {"type": "number"}}}',
'data': '"foobar"',
}),
("property_checks_known_props", {
'schema': """
{
"type": "object",
"properties": {
"foo": {
"type": "number"
},
"bar": {
"type": "boolean"
}
}
}""",
'data': """
{
"foo": 5,
"bar": false
}"""
}),
("property_check_ignores_missing_optional_properties", {
'schema': """
{
"type": "object",
"properties": {
"foo": {
"type": "number",
"optional": true
}
}
}""",
'data': '{}',
}),
("property_check_ignores_normal_properties_when_additionalProperties_is_false", {
'schema': """
{
"type": "object",
"properties": {
"foo": {}
},
"additionalProperties": false
}""",
'data': '{"foo": 5}',
}),
("property_check_validates_additional_properties_using_specified_type_when_additionalProperties_is_an_object", {
'schema': """
{
"type": "object",
"additionalProperties": {
"type": "string"
}
}""",
'data': '{"foo": "aaa", "bar": "bbb"}',
}),
("enum_check_does_nothing_by_default", {
'schema': '{}',
'data': '5',
}),
("enum_check_verifies_possible_values", {
'schema': '{"enum": [1, 2, 3]}',
'data': '2',
}),
("items_check_does_nothing_for_non_arrays", {
'schema': '{"items": {"type": "string"}}',
'data': '5',
}),
("items_with_single_schema_applies_to_each_item", {
'schema': '{"items": {"type": "string"}}',
'data': '["foo", "bar", "froz"]',
}),
("items_with_array_schema_applies_to_corresponding_items", {
'schema': """
{
"items": [
{"type": "string"},
{"type": "boolean"}
]
}""",
'data': '["foo", true]',
}),
("items_with_array_schema_and_additionalProperties", {
'schema': """
{
"items": [
{"type": "string"},
{"type": "boolean"}
],
"additionalProperties": {
"type": "number"
}
}""",
'data': '["foo", false, 5, 7.9]',
}),
("requires_with_simple_property_name_does_nothing_when_parent_property_is_not_used", {
'schema': """
{
"properties": {
"foo": {
"optional": true
},
"bar": {
"requires": "foo",
"optional": true
}
}
}
""",
'data': '{}',
}),
("requires_with_simple_property_name_works_when_condition_satisfied", {
'schema': """
{
"properties": {
"foo": {
"optional": true
},
"bar": {
"requires": "foo",
"optional": true
}
}
}
""",
'data': '{"foo": null, "bar": null}',
}),
("requires_with_schema_name_does_nothing_when_parent_property_is_not_used", {
'schema': """
{
"properties": {
"foo": {
"optional": true
},
"bar": {
"requires": {
"properties": {
"foo": {
"type": "number"
}
}
},
"optional": true
}
}
}
""",
'data': '{}',
}),
("format_date_time_works", {
'schema': '{"format": "date-time"}',
'data': '"2010-11-12T14:38:55Z"',
}),
("array_with_array_schema_and_uniqueItems_is_True", {
'schema': """
{
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
}""",
'data': '["foo", "bar", "baz"]',
}),
]
def test_validator_does_not_raise_an_exception(self):
self.assertEqual(
True, validate(self.schema, self.data))