Merge "V3 jsonschema validation: base schema framework"
This commit is contained in:
commit
ab5dfc096e
0
cinder/api/schemas/__init__.py
Normal file
0
cinder/api/schemas/__init__.py
Normal file
64
cinder/api/validation/__init__.py
Normal file
64
cinder/api/validation/__init__.py
Normal file
@ -0,0 +1,64 @@
|
||||
# Copyright (C) 2017 NTT DATA
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Request Body validating middleware.
|
||||
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
from cinder.api.openstack import api_version_request as api_version
|
||||
from cinder.api.validation import validators
|
||||
|
||||
|
||||
def schema(request_body_schema, min_version=None, max_version=None):
|
||||
"""Register a schema to validate request body.
|
||||
|
||||
Registered schema will be used for validating request body just before
|
||||
API method executing.
|
||||
|
||||
:param dict request_body_schema: a schema to validate request body
|
||||
:param min_version: A string of two numerals. X.Y indicating the minimum
|
||||
version of the JSON-Schema to validate against.
|
||||
:param max_version: A string of two numerals. X.Y indicating the maximum
|
||||
version of the JSON-Schema to validate against.
|
||||
|
||||
"""
|
||||
|
||||
def add_validator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
min_ver = api_version.APIVersionRequest(min_version)
|
||||
max_ver = api_version.APIVersionRequest(max_version)
|
||||
|
||||
if 'req' in kwargs:
|
||||
ver = kwargs['req'].api_version_request
|
||||
else:
|
||||
ver = args[1].api_version_request
|
||||
|
||||
if ver.matches(min_ver, max_ver):
|
||||
# Only validate against the schema if it lies within
|
||||
# the version range specified. Note that if both min
|
||||
# and max are not specified the validator will always
|
||||
# be run.
|
||||
schema_validator = validators._SchemaValidator(
|
||||
request_body_schema)
|
||||
schema_validator.validate(kwargs['body'])
|
||||
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
return add_validator
|
137
cinder/api/validation/parameter_types.py
Normal file
137
cinder/api/validation/parameter_types.py
Normal file
@ -0,0 +1,137 @@
|
||||
# Copyright (C) 2017 NTT DATA
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Common parameter types for validating request Body.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
import six
|
||||
|
||||
|
||||
def _is_printable(char):
|
||||
"""determine if a unicode code point is printable.
|
||||
|
||||
This checks if the character is either "other" (mostly control
|
||||
codes), or a non-horizontal space. All characters that don't match
|
||||
those criteria are considered printable; that is: letters;
|
||||
combining marks; numbers; punctuation; symbols; (horizontal) space
|
||||
separators.
|
||||
"""
|
||||
category = unicodedata.category(char)
|
||||
return (not category.startswith("C") and
|
||||
(not category.startswith("Z") or category == "Zs"))
|
||||
|
||||
|
||||
def _get_all_chars():
|
||||
for i in range(0xFFFF):
|
||||
yield six.unichr(i)
|
||||
|
||||
|
||||
# build a regex that matches all printable characters. This allows
|
||||
# spaces in the middle of the name. Also note that the regexp below
|
||||
# deliberately allows the empty string. This is so only the constraint
|
||||
# which enforces a minimum length for the name is triggered when an
|
||||
# empty string is tested. Otherwise it is not deterministic which
|
||||
# constraint fails and this causes issues for some unittests when
|
||||
# PYTHONHASHSEED is set randomly.
|
||||
|
||||
def _build_regex_range(ws=True, invert=False, exclude=None):
|
||||
"""Build a range regex for a set of characters in utf8.
|
||||
|
||||
This builds a valid range regex for characters in utf8 by
|
||||
iterating the entire space and building up a set of x-y ranges for
|
||||
all the characters we find which are valid.
|
||||
|
||||
:param ws: should we include whitespace in this range.
|
||||
:param exclude: any characters we want to exclude
|
||||
:param invert: invert the logic
|
||||
|
||||
The inversion is useful when we want to generate a set of ranges
|
||||
which is everything that's not a certain class. For instance,
|
||||
produce all all the non printable characters as a set of ranges.
|
||||
"""
|
||||
if exclude is None:
|
||||
exclude = []
|
||||
regex = ""
|
||||
# are we currently in a range
|
||||
in_range = False
|
||||
# last character we found, for closing ranges
|
||||
last = None
|
||||
# last character we added to the regex, this lets us know that we
|
||||
# already have B in the range, which means we don't need to close
|
||||
# it out with B-B. While the later seems to work, it's kind of bad form.
|
||||
last_added = None
|
||||
|
||||
def valid_char(char):
|
||||
if char in exclude:
|
||||
result = False
|
||||
elif ws:
|
||||
result = _is_printable(char)
|
||||
else:
|
||||
# Zs is the unicode class for space characters, of which
|
||||
# there are about 10 in this range.
|
||||
result = (_is_printable(char) and
|
||||
unicodedata.category(char) != "Zs")
|
||||
if invert is True:
|
||||
return not result
|
||||
return result
|
||||
|
||||
# iterate through the entire character range. in_
|
||||
for c in _get_all_chars():
|
||||
if valid_char(c):
|
||||
if not in_range:
|
||||
regex += re.escape(c)
|
||||
last_added = c
|
||||
in_range = True
|
||||
else:
|
||||
if in_range and last != last_added:
|
||||
regex += "-" + re.escape(last)
|
||||
in_range = False
|
||||
last = c
|
||||
else:
|
||||
if in_range:
|
||||
regex += "-" + re.escape(c)
|
||||
return regex
|
||||
|
||||
|
||||
valid_description_regex_base = '^[%s]*$'
|
||||
|
||||
valid_description_regex = valid_description_regex_base % (
|
||||
_build_regex_range())
|
||||
|
||||
|
||||
name = {
|
||||
'type': 'string', 'minLength': 1, 'maxLength': 255,
|
||||
'format': 'name'
|
||||
}
|
||||
|
||||
|
||||
description = {
|
||||
'type': ['string', 'null'], 'minLength': 0, 'maxLength': 255,
|
||||
'pattern': valid_description_regex,
|
||||
}
|
||||
|
||||
|
||||
boolean = {
|
||||
'type': ['boolean', 'string'],
|
||||
'enum': [True, 'True', 'TRUE', 'true', '1', 'ON', 'On', 'on',
|
||||
'YES', 'Yes', 'yes', 'y', 't',
|
||||
False, 'False', 'FALSE', 'false', '0', 'OFF', 'Off', 'off',
|
||||
'NO', 'No', 'no', 'n', 'f'],
|
||||
}
|
211
cinder/api/validation/validators.py
Normal file
211
cinder/api/validation/validators.py
Normal file
@ -0,0 +1,211 @@
|
||||
# Copyright (C) 2017 NTT DATA
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Internal implementation of request Body validating middleware.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
import jsonschema
|
||||
from jsonschema import exceptions as jsonschema_exc
|
||||
from oslo_utils import timeutils
|
||||
import six
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
|
||||
|
||||
def _soft_validate_additional_properties(
|
||||
validator, additional_properties_value, param_value, schema):
|
||||
"""Validator function.
|
||||
|
||||
If there are not any properties on the param_value that are not specified
|
||||
in the schema, this will return without any effect. If there are any such
|
||||
extra properties, they will be handled as follows:
|
||||
|
||||
- if the validator passed to the method is not of type "object", this
|
||||
method will return without any effect.
|
||||
- if the 'additional_properties_value' parameter is True, this method will
|
||||
return without any effect.
|
||||
- if the schema has an additionalProperties value of True, the extra
|
||||
properties on the param_value will not be touched.
|
||||
- if the schema has an additionalProperties value of False and there
|
||||
aren't patternProperties specified, the extra properties will be stripped
|
||||
from the param_value.
|
||||
- if the schema has an additionalProperties value of False and there
|
||||
are patternProperties specified, the extra properties will not be
|
||||
touched and raise validation error if pattern doesn't match.
|
||||
"""
|
||||
if (not validator.is_type(param_value, "object") or
|
||||
additional_properties_value):
|
||||
return
|
||||
|
||||
properties = schema.get("properties", {})
|
||||
patterns = "|".join(schema.get("patternProperties", {}))
|
||||
extra_properties = set()
|
||||
for prop in param_value:
|
||||
if prop not in properties:
|
||||
if patterns:
|
||||
if not re.search(patterns, prop):
|
||||
extra_properties.add(prop)
|
||||
else:
|
||||
extra_properties.add(prop)
|
||||
|
||||
if not extra_properties:
|
||||
return
|
||||
|
||||
if patterns:
|
||||
error = "Additional properties are not allowed (%s %s unexpected)"
|
||||
if len(extra_properties) == 1:
|
||||
verb = "was"
|
||||
else:
|
||||
verb = "were"
|
||||
yield jsonschema_exc.ValidationError(
|
||||
error % (", ".join(repr(extra) for extra in extra_properties),
|
||||
verb))
|
||||
else:
|
||||
for prop in extra_properties:
|
||||
del param_value[prop]
|
||||
|
||||
|
||||
@jsonschema.FormatChecker.cls_checks('date-time')
|
||||
def _validate_datetime_format(param_value):
|
||||
try:
|
||||
timeutils.parse_isotime(param_value)
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
@jsonschema.FormatChecker.cls_checks('name', exception.InvalidName)
|
||||
def _validate_name(param_value):
|
||||
if not param_value:
|
||||
msg = "The 'name' can not be None."
|
||||
raise exception.InvalidName(reason=msg)
|
||||
elif len(param_value.strip()) == 0:
|
||||
msg = "The 'name' can not be empty."
|
||||
raise exception.InvalidName(reason=msg)
|
||||
return True
|
||||
|
||||
|
||||
class FormatChecker(jsonschema.FormatChecker):
|
||||
"""A FormatChecker can output the message from cause exception
|
||||
|
||||
We need understandable validation errors messages for users. When a
|
||||
custom checker has an exception, the FormatChecker will output a
|
||||
readable message provided by the checker.
|
||||
"""
|
||||
|
||||
def check(self, param_value, format):
|
||||
"""Check whether the param_value conforms to the given format.
|
||||
|
||||
:argument param_value: the param_value to check
|
||||
:type: any primitive type (str, number, bool)
|
||||
:argument str format: the format that param_value should conform to
|
||||
:raises: :exc:`FormatError` if param_value does not conform to format
|
||||
"""
|
||||
|
||||
if format not in self.checkers:
|
||||
return
|
||||
|
||||
# For safety reasons custom checkers can be registered with
|
||||
# allowed exception types. Anything else will fall into the
|
||||
# default formatter.
|
||||
func, raises = self.checkers[format]
|
||||
result, cause = None, None
|
||||
|
||||
try:
|
||||
result = func(param_value)
|
||||
except raises as e:
|
||||
cause = e
|
||||
if not result:
|
||||
msg = "%r is not a %r" % (param_value, format)
|
||||
raise jsonschema_exc.FormatError(msg, cause=cause)
|
||||
|
||||
|
||||
class _SchemaValidator(object):
|
||||
"""A validator class
|
||||
|
||||
This class is changed from Draft4Validator to validate minimum/maximum
|
||||
value of a string number(e.g. '10'). This changes can be removed when
|
||||
we tighten up the API definition and the XML conversion.
|
||||
Also FormatCheckers are added for checking data formats which would be
|
||||
passed through cinder api commonly.
|
||||
|
||||
"""
|
||||
validator = None
|
||||
validator_org = jsonschema.Draft4Validator
|
||||
|
||||
def __init__(self, schema, relax_additional_properties=False):
|
||||
validators = {
|
||||
'minimum': self._validate_minimum,
|
||||
'maximum': self._validate_maximum,
|
||||
}
|
||||
if relax_additional_properties:
|
||||
validators[
|
||||
'additionalProperties'] = _soft_validate_additional_properties
|
||||
|
||||
validator_cls = jsonschema.validators.extend(self.validator_org,
|
||||
validators)
|
||||
format_checker = FormatChecker()
|
||||
self.validator = validator_cls(schema, format_checker=format_checker)
|
||||
|
||||
def validate(self, *args, **kwargs):
|
||||
try:
|
||||
self.validator.validate(*args, **kwargs)
|
||||
except jsonschema.ValidationError as ex:
|
||||
if isinstance(ex.cause, exception.InvalidName):
|
||||
detail = ex.cause.msg
|
||||
elif len(ex.path) > 0:
|
||||
detail = _("Invalid input for field/attribute %(path)s."
|
||||
" Value: %(value)s. %(message)s") % {
|
||||
'path': ex.path.pop(), 'value': ex.instance,
|
||||
'message': ex.message
|
||||
}
|
||||
else:
|
||||
detail = ex.message
|
||||
raise exception.ValidationError(detail=detail)
|
||||
except TypeError as ex:
|
||||
# NOTE: If passing non string value to patternProperties parameter,
|
||||
# TypeError happens. Here is for catching the TypeError.
|
||||
detail = six.text_type(ex)
|
||||
raise exception.ValidationError(detail=detail)
|
||||
|
||||
def _number_from_str(self, param_value):
|
||||
try:
|
||||
value = int(param_value)
|
||||
except (ValueError, TypeError):
|
||||
try:
|
||||
value = float(param_value)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
return value
|
||||
|
||||
def _validate_minimum(self, validator, minimum, param_value, schema):
|
||||
param_value = self._number_from_str(param_value)
|
||||
if param_value is None:
|
||||
return
|
||||
return self.validator_org.VALIDATORS['minimum'](validator, minimum,
|
||||
param_value, schema)
|
||||
|
||||
def _validate_maximum(self, validator, maximum, param_value, schema):
|
||||
param_value = self._number_from_str(param_value)
|
||||
if param_value is None:
|
||||
return
|
||||
return self.validator_org.VALIDATORS['maximum'](validator, maximum,
|
||||
param_value, schema)
|
@ -1336,3 +1336,7 @@ class ErrorInParsingArguments(VolumeDriverException):
|
||||
# GPFS driver
|
||||
class GPFSDriverUnsupportedOperation(VolumeBackendAPIException):
|
||||
message = _("GPFS driver unsupported operation: %(msg)s")
|
||||
|
||||
|
||||
class InvalidName(Invalid):
|
||||
message = _("An invalid 'name' value was provided. %(reason)s")
|
||||
|
505
cinder/tests/unit/api/test_api_validation.py
Normal file
505
cinder/tests/unit/api/test_api_validation.py
Normal file
@ -0,0 +1,505 @@
|
||||
# Copyright (C) 2017 NTT DATA
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
import fixtures
|
||||
import six
|
||||
from six.moves import http_client as http
|
||||
|
||||
from cinder.api.openstack import api_version_request as api_version
|
||||
from cinder.api import validation
|
||||
from cinder.api.validation import parameter_types
|
||||
from cinder import exception
|
||||
from cinder import test
|
||||
|
||||
|
||||
class FakeRequest(object):
|
||||
api_version_request = api_version.APIVersionRequest("3.0")
|
||||
environ = {}
|
||||
|
||||
|
||||
class ValidationRegex(test.TestCase):
|
||||
|
||||
def test_build_regex_range(self):
|
||||
|
||||
def _get_all_chars():
|
||||
for i in range(0x7F):
|
||||
yield six.unichr(i)
|
||||
|
||||
self.useFixture(fixtures.MonkeyPatch(
|
||||
'cinder.api.validation.parameter_types._get_all_chars',
|
||||
_get_all_chars))
|
||||
|
||||
r = parameter_types._build_regex_range(ws=False)
|
||||
self.assertEqual(re.escape('!') + '-' + re.escape('~'), r)
|
||||
|
||||
# if we allow whitespace the range starts earlier
|
||||
r = parameter_types._build_regex_range(ws=True)
|
||||
self.assertEqual(re.escape(' ') + '-' + re.escape('~'), r)
|
||||
|
||||
# excluding a character will give us 2 ranges
|
||||
r = parameter_types._build_regex_range(ws=True, exclude=['A'])
|
||||
self.assertEqual(re.escape(' ') + '-' + re.escape('@') +
|
||||
'B' + '-' + re.escape('~'), r)
|
||||
|
||||
# inverting which gives us all the initial unprintable characters.
|
||||
r = parameter_types._build_regex_range(ws=False, invert=True)
|
||||
self.assertEqual(re.escape('\x00') + '-' + re.escape(' '), r)
|
||||
|
||||
# excluding characters that create a singleton. Naively this would be:
|
||||
# ' -@B-BD-~' which seems to work, but ' -@BD-~' is more natural.
|
||||
r = parameter_types._build_regex_range(ws=True, exclude=['A', 'C'])
|
||||
self.assertEqual(re.escape(' ') + '-' + re.escape('@') +
|
||||
'B' + 'D' + '-' + re.escape('~'), r)
|
||||
|
||||
# ws=True means the positive regex has printable whitespaces,
|
||||
# so the inverse will not. The inverse will include things we
|
||||
# exclude.
|
||||
r = parameter_types._build_regex_range(
|
||||
ws=True, exclude=['A', 'B', 'C', 'Z'], invert=True)
|
||||
self.assertEqual(re.escape('\x00') + '-' + re.escape('\x1f') +
|
||||
'A-CZ', r)
|
||||
|
||||
|
||||
class APIValidationTestCase(test.TestCase):
|
||||
|
||||
def setUp(self, schema=None):
|
||||
super(APIValidationTestCase, self).setUp()
|
||||
self.post = None
|
||||
|
||||
if schema is not None:
|
||||
@validation.schema(request_body_schema=schema)
|
||||
def post(req, body):
|
||||
return 'Validation succeeded.'
|
||||
|
||||
self.post = post
|
||||
|
||||
def check_validation_error(self, method, body, expected_detail, req=None):
|
||||
if not req:
|
||||
req = FakeRequest()
|
||||
try:
|
||||
method(body=body, req=req,)
|
||||
except exception.ValidationError as ex:
|
||||
self.assertEqual(http.BAD_REQUEST, ex.kwargs['code'])
|
||||
if isinstance(expected_detail, list):
|
||||
self.assertIn(ex.kwargs['detail'], expected_detail,
|
||||
'Exception details did not match expected')
|
||||
elif not re.match(expected_detail, ex.kwargs['detail']):
|
||||
self.assertEqual(expected_detail, ex.kwargs['detail'],
|
||||
'Exception details did not match expected')
|
||||
except Exception as ex:
|
||||
self.fail('An unexpected exception happens: %s' % ex)
|
||||
else:
|
||||
self.fail('Any exception did not happen.')
|
||||
|
||||
|
||||
class RequiredDisableTestCase(APIValidationTestCase):
|
||||
|
||||
def setUp(self):
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'foo': {
|
||||
'type': 'integer',
|
||||
},
|
||||
},
|
||||
}
|
||||
super(RequiredDisableTestCase, self).setUp(schema=schema)
|
||||
|
||||
def test_validate_required_disable(self):
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 1}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'abc': 1}, req=FakeRequest()))
|
||||
|
||||
|
||||
class RequiredEnableTestCase(APIValidationTestCase):
|
||||
|
||||
def setUp(self):
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'foo': {
|
||||
'type': 'integer',
|
||||
},
|
||||
},
|
||||
'required': ['foo']
|
||||
}
|
||||
super(RequiredEnableTestCase, self).setUp(schema=schema)
|
||||
|
||||
def test_validate_required_enable(self):
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 1}, req=FakeRequest()))
|
||||
|
||||
def test_validate_required_enable_fails(self):
|
||||
detail = "'foo' is a required property"
|
||||
self.check_validation_error(self.post, body={'abc': 1},
|
||||
expected_detail=detail)
|
||||
|
||||
|
||||
class AdditionalPropertiesEnableTestCase(APIValidationTestCase):
|
||||
|
||||
def setUp(self):
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'foo': {
|
||||
'type': 'integer',
|
||||
},
|
||||
},
|
||||
'required': ['foo'],
|
||||
}
|
||||
super(AdditionalPropertiesEnableTestCase, self).setUp(schema=schema)
|
||||
|
||||
def test_validate_additionalProperties_enable(self):
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 1}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 1, 'ext': 1},
|
||||
req=FakeRequest()))
|
||||
|
||||
|
||||
class AdditionalPropertiesDisableTestCase(APIValidationTestCase):
|
||||
|
||||
def setUp(self):
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'foo': {
|
||||
'type': 'integer',
|
||||
},
|
||||
},
|
||||
'required': ['foo'],
|
||||
'additionalProperties': False,
|
||||
}
|
||||
super(AdditionalPropertiesDisableTestCase, self).setUp(schema=schema)
|
||||
|
||||
def test_validate_additionalProperties_disable(self):
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 1}, req=FakeRequest()))
|
||||
|
||||
def test_validate_additionalProperties_disable_fails(self):
|
||||
detail = "Additional properties are not allowed ('ext' was unexpected)"
|
||||
self.check_validation_error(self.post, body={'foo': 1, 'ext': 1},
|
||||
expected_detail=detail)
|
||||
|
||||
|
||||
class PatternPropertiesTestCase(APIValidationTestCase):
|
||||
|
||||
def setUp(self):
|
||||
schema = {
|
||||
'patternProperties': {
|
||||
'^[a-zA-Z0-9]{1,10}$': {
|
||||
'type': 'string'
|
||||
},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
}
|
||||
super(PatternPropertiesTestCase, self).setUp(schema=schema)
|
||||
|
||||
def test_validate_patternProperties(self):
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 'bar'}, req=FakeRequest()))
|
||||
|
||||
def test_validate_patternProperties_fails(self):
|
||||
details = [
|
||||
"Additional properties are not allowed ('__' was unexpected)",
|
||||
"'__' does not match any of the regexes: '^[a-zA-Z0-9]{1,10}$'"
|
||||
]
|
||||
self.check_validation_error(self.post, body={'__': 'bar'},
|
||||
expected_detail=details)
|
||||
|
||||
details = [
|
||||
"'' does not match any of the regexes: '^[a-zA-Z0-9]{1,10}$'",
|
||||
"Additional properties are not allowed ('' was unexpected)"
|
||||
]
|
||||
self.check_validation_error(self.post, body={'': 'bar'},
|
||||
expected_detail=details)
|
||||
|
||||
details = [
|
||||
("'0123456789a' does not match any of the regexes: "
|
||||
"'^[a-zA-Z0-9]{1,10}$'"),
|
||||
("Additional properties are not allowed ('0123456789a' was"
|
||||
" unexpected)")
|
||||
]
|
||||
self.check_validation_error(self.post, body={'0123456789a': 'bar'},
|
||||
expected_detail=details)
|
||||
|
||||
if sys.version[:3] == '3.5':
|
||||
detail = "expected string or bytes-like object"
|
||||
else:
|
||||
detail = "expected string or buffer"
|
||||
self.check_validation_error(self.post, body={None: 'bar'},
|
||||
expected_detail=detail)
|
||||
|
||||
|
||||
class StringTestCase(APIValidationTestCase):
|
||||
|
||||
def setUp(self):
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'foo': {
|
||||
'type': 'string',
|
||||
},
|
||||
},
|
||||
}
|
||||
super(StringTestCase, self).setUp(schema=schema)
|
||||
|
||||
def test_validate_string(self):
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 'abc'}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': '0'}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': ''}, req=FakeRequest()))
|
||||
|
||||
def test_validate_string_fails(self):
|
||||
detail = ("Invalid input for field/attribute foo. Value: 1."
|
||||
" 1 is not of type 'string'")
|
||||
self.check_validation_error(self.post, body={'foo': 1},
|
||||
expected_detail=detail)
|
||||
|
||||
detail = ("Invalid input for field/attribute foo. Value: 1.5."
|
||||
" 1.5 is not of type 'string'")
|
||||
self.check_validation_error(self.post, body={'foo': 1.5},
|
||||
expected_detail=detail)
|
||||
|
||||
detail = ("Invalid input for field/attribute foo. Value: True."
|
||||
" True is not of type 'string'")
|
||||
self.check_validation_error(self.post, body={'foo': True},
|
||||
expected_detail=detail)
|
||||
|
||||
|
||||
class StringLengthTestCase(APIValidationTestCase):
|
||||
|
||||
def setUp(self):
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'foo': {
|
||||
'type': 'string',
|
||||
'minLength': 1,
|
||||
'maxLength': 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
super(StringLengthTestCase, self).setUp(schema=schema)
|
||||
|
||||
def test_validate_string_length(self):
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': '0'}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': '0123456789'},
|
||||
req=FakeRequest()))
|
||||
|
||||
def test_validate_string_length_fails(self):
|
||||
detail = ("Invalid input for field/attribute foo. Value: ."
|
||||
" '' is too short")
|
||||
self.check_validation_error(self.post, body={'foo': ''},
|
||||
expected_detail=detail)
|
||||
|
||||
detail = ("Invalid input for field/attribute foo. Value: 0123456789a."
|
||||
" '0123456789a' is too long")
|
||||
self.check_validation_error(self.post, body={'foo': '0123456789a'},
|
||||
expected_detail=detail)
|
||||
|
||||
|
||||
class IntegerTestCase(APIValidationTestCase):
|
||||
|
||||
def setUp(self):
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'foo': {
|
||||
'type': ['integer', 'string'],
|
||||
'pattern': '^[0-9]+$',
|
||||
},
|
||||
},
|
||||
}
|
||||
super(IntegerTestCase, self).setUp(schema=schema)
|
||||
|
||||
def test_validate_integer(self):
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 1}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': '1'}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': '0123456789'},
|
||||
req=FakeRequest()))
|
||||
|
||||
def test_validate_integer_fails(self):
|
||||
detail = ("Invalid input for field/attribute foo. Value: abc."
|
||||
" 'abc' does not match '^[0-9]+$'")
|
||||
self.check_validation_error(self.post, body={'foo': 'abc'},
|
||||
expected_detail=detail)
|
||||
|
||||
detail = ("Invalid input for field/attribute foo. Value: True."
|
||||
" True is not of type 'integer', 'string'")
|
||||
self.check_validation_error(self.post, body={'foo': True},
|
||||
expected_detail=detail)
|
||||
|
||||
detail = ("Invalid input for field/attribute foo. Value: 0xffff."
|
||||
" '0xffff' does not match '^[0-9]+$'")
|
||||
self.check_validation_error(self.post, body={'foo': '0xffff'},
|
||||
expected_detail=detail)
|
||||
|
||||
detail = ("Invalid input for field/attribute foo. Value: 1.0."
|
||||
" 1.0 is not of type 'integer', 'string'")
|
||||
self.check_validation_error(self.post, body={'foo': 1.0},
|
||||
expected_detail=detail)
|
||||
|
||||
detail = ("Invalid input for field/attribute foo. Value: 1.0."
|
||||
" '1.0' does not match '^[0-9]+$'")
|
||||
self.check_validation_error(self.post, body={'foo': '1.0'},
|
||||
expected_detail=detail)
|
||||
|
||||
|
||||
class IntegerRangeTestCase(APIValidationTestCase):
|
||||
|
||||
def setUp(self):
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'foo': {
|
||||
'type': ['integer', 'string'],
|
||||
'pattern': '^[0-9]+$',
|
||||
'minimum': 1,
|
||||
'maximum': 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
super(IntegerRangeTestCase, self).setUp(schema=schema)
|
||||
|
||||
def test_validate_integer_range(self):
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 1}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 10}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': '1'}, req=FakeRequest()))
|
||||
|
||||
def test_validate_integer_range_fails(self):
|
||||
detail = ("Invalid input for field/attribute foo. Value: 0."
|
||||
" 0(.0)? is less than the minimum of 1")
|
||||
self.check_validation_error(self.post, body={'foo': 0},
|
||||
expected_detail=detail)
|
||||
|
||||
detail = ("Invalid input for field/attribute foo. Value: 11."
|
||||
" 11(.0)? is greater than the maximum of 10")
|
||||
self.check_validation_error(self.post, body={'foo': 11},
|
||||
expected_detail=detail)
|
||||
|
||||
detail = ("Invalid input for field/attribute foo. Value: 0."
|
||||
" 0(.0)? is less than the minimum of 1")
|
||||
self.check_validation_error(self.post, body={'foo': '0'},
|
||||
expected_detail=detail)
|
||||
|
||||
detail = ("Invalid input for field/attribute foo. Value: 11."
|
||||
" 11(.0)? is greater than the maximum of 10")
|
||||
self.check_validation_error(self.post, body={'foo': '11'},
|
||||
expected_detail=detail)
|
||||
|
||||
|
||||
class BooleanTestCase(APIValidationTestCase):
|
||||
|
||||
def setUp(self):
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'foo': parameter_types.boolean,
|
||||
},
|
||||
}
|
||||
super(BooleanTestCase, self).setUp(schema=schema)
|
||||
|
||||
def test_validate_boolean(self):
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': True}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': False}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 'True'}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 'False'}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': '1'}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': '0'}, req=FakeRequest()))
|
||||
|
||||
def test_validate_boolean_fails(self):
|
||||
enum_boolean = ("[True, 'True', 'TRUE', 'true', '1', 'ON', 'On',"
|
||||
" 'on', 'YES', 'Yes', 'yes', 'y', 't',"
|
||||
" False, 'False', 'FALSE', 'false', '0', 'OFF', 'Off',"
|
||||
" 'off', 'NO', 'No', 'no', 'n', 'f']")
|
||||
|
||||
detail = ("Invalid input for field/attribute foo. Value: bar."
|
||||
" 'bar' is not one of %s") % enum_boolean
|
||||
self.check_validation_error(self.post, body={'foo': 'bar'},
|
||||
expected_detail=detail)
|
||||
|
||||
detail = ("Invalid input for field/attribute foo. Value: 2."
|
||||
" '2' is not one of %s") % enum_boolean
|
||||
self.check_validation_error(self.post, body={'foo': '2'},
|
||||
expected_detail=detail)
|
||||
|
||||
|
||||
class NameTestCase(APIValidationTestCase):
|
||||
|
||||
def setUp(self):
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'foo': parameter_types.name,
|
||||
},
|
||||
}
|
||||
super(NameTestCase, self).setUp(schema=schema)
|
||||
|
||||
def test_validate_name(self):
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 'volume.1'},
|
||||
req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 'volume 1'},
|
||||
req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 'a'}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': u'\u0434'}, req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': u'\u0434\u2006\ufffd'},
|
||||
req=FakeRequest()))
|
||||
|
||||
|
||||
class DatetimeTestCase(APIValidationTestCase):
|
||||
|
||||
def setUp(self):
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'foo': {
|
||||
'type': 'string',
|
||||
'format': 'date-time',
|
||||
},
|
||||
},
|
||||
}
|
||||
super(DatetimeTestCase, self).setUp(schema=schema)
|
||||
|
||||
def test_validate_datetime(self):
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={
|
||||
'foo': '2017-01-14T01:00:00Z'}, req=FakeRequest()
|
||||
))
|
@ -10,6 +10,7 @@ eventlet!=0.18.3,!=0.20.1,<0.21.0,>=0.18.2 # MIT
|
||||
greenlet>=0.4.10 # MIT
|
||||
httplib2>=0.9.1 # MIT
|
||||
iso8601>=0.1.11 # MIT
|
||||
jsonschema<3.0.0,>=2.6.0 # MIT
|
||||
ipaddress>=1.0.16;python_version<'3.3' # PSF
|
||||
keystoneauth1>=3.2.0 # Apache-2.0
|
||||
keystonemiddleware>=4.17.0 # Apache-2.0
|
||||
|
Loading…
x
Reference in New Issue
Block a user