Browse Source

V3 jsonschema validation: base schema framework

This patch adds a base jsonschema framework which will be used
by other subsequent patches for adding jsonschema validation
support for v3 APIs.

This patch follows the Nova-Schema-framework:
https://github.com/openstack/nova/tree/master/nova/api/validation

Depends-On: I4b1140c20c68821eaf0849c9ee551ff9b1c27deb
Partial-Implements: bp json-schema-validation
Co-author: Pooja Jadhav <pooja.jadhav@nttdata.com>
Change-Id: If40029a6ab9b15c292f9297f5bf56aec9621a7d9
changes/49/515649/12
Dinesh Bhor 4 years ago
committed by pooja jadhav
parent
commit
5420c0bf5f
  1. 0
      cinder/api/schemas/__init__.py
  2. 64
      cinder/api/validation/__init__.py
  3. 137
      cinder/api/validation/parameter_types.py
  4. 211
      cinder/api/validation/validators.py
  5. 4
      cinder/exception.py
  6. 505
      cinder/tests/unit/api/test_api_validation.py
  7. 1
      requirements.txt

0
cinder/api/schemas/__init__.py

64
cinder/api/validation/__init__.py

@ -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

@ -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

@ -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)

4
cinder/exception.py

@ -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

@ -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()
))

1
requirements.txt

@ -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…
Cancel
Save