Merge "New argument validate decorator"
This commit is contained in:
commit
0544291e2f
|
@ -0,0 +1,394 @@
|
|||
# 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 functools
|
||||
import inspect
|
||||
|
||||
import jsonschema
|
||||
from oslo_utils import strutils
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import utils
|
||||
|
||||
|
||||
def string(name, value):
|
||||
"""Validate that the value is a string
|
||||
|
||||
:param name: Name of the argument
|
||||
:param value: A string value
|
||||
:returns: The string value, or None if value is None
|
||||
:raises: InvalidParameterValue if the value is not a string
|
||||
"""
|
||||
if value is None:
|
||||
return
|
||||
if not isinstance(value, str):
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Expected string for %s: %s') % (name, value))
|
||||
return value
|
||||
|
||||
|
||||
def boolean(name, value):
|
||||
"""Validate that the value is a string representing a boolean
|
||||
|
||||
:param name: Name of the argument
|
||||
:param value: A string value
|
||||
:returns: The boolean representation of the value, or None if value is None
|
||||
:raises: InvalidParameterValue if the value cannot be converted to a
|
||||
boolean
|
||||
"""
|
||||
if value is None:
|
||||
return
|
||||
try:
|
||||
return strutils.bool_from_string(value, strict=True)
|
||||
except ValueError as e:
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Invalid %s: %s') % (name, e))
|
||||
|
||||
|
||||
def uuid(name, value):
|
||||
"""Validate that the value is a UUID
|
||||
|
||||
:param name: Name of the argument
|
||||
:param value: A UUID string value
|
||||
:returns: The value, or None if value is None
|
||||
:raises: InvalidParameterValue if the value is not a valid UUID
|
||||
"""
|
||||
if value is None:
|
||||
return
|
||||
if not uuidutils.is_uuid_like(value):
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Expected UUID for %s: %s') % (name, value))
|
||||
return value
|
||||
|
||||
|
||||
def name(name, value):
|
||||
"""Validate that the value is a logical name
|
||||
|
||||
:param name: Name of the argument
|
||||
:param value: A logical name string value
|
||||
:returns: The value, or None if value is None
|
||||
:raises: InvalidParameterValue if the value is not a valid logical name
|
||||
"""
|
||||
if value is None:
|
||||
return
|
||||
if not utils.is_valid_logical_name(value):
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Expected name for %s: %s') % (name, value))
|
||||
return value
|
||||
|
||||
|
||||
def uuid_or_name(name, value):
|
||||
"""Validate that the value is a UUID or logical name
|
||||
|
||||
:param name: Name of the argument
|
||||
:param value: A UUID or logical name string value
|
||||
:returns: The value, or None if value is None
|
||||
:raises: InvalidParameterValue if the value is not a valid UUID or
|
||||
logical name
|
||||
"""
|
||||
if value is None:
|
||||
return
|
||||
if (not utils.is_valid_logical_name(value)
|
||||
and not uuidutils.is_uuid_like(value)):
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Expected UUID or name for %s: %s') % (name, value))
|
||||
return value
|
||||
|
||||
|
||||
def string_list(name, value):
|
||||
"""Validate and convert comma delimited string to a list.
|
||||
|
||||
:param name: Name of the argument
|
||||
:param value: A comma separated string of values
|
||||
:returns: A list of unique values (lower-cased), maintaining the
|
||||
same order, or None if value is None
|
||||
:raises: InvalidParameterValue if the value is not a string
|
||||
"""
|
||||
value = string(name, value)
|
||||
if value is None:
|
||||
return
|
||||
items = []
|
||||
for v in str(value).split(','):
|
||||
v_norm = v.strip().lower()
|
||||
if v_norm and v_norm not in items:
|
||||
items.append(v_norm)
|
||||
return items
|
||||
|
||||
|
||||
def integer(name, value):
|
||||
"""Validate that the value represents an integer
|
||||
|
||||
:param name: Name of the argument
|
||||
:param value: A value representing an integer
|
||||
:returns: The value as an int, or None if value is None
|
||||
:raises: InvalidParameterValue if the value does not represent an integer
|
||||
"""
|
||||
if value is None:
|
||||
return
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Expected an integer for %s: %s') % (name, value))
|
||||
|
||||
|
||||
def mac_address(name, value):
|
||||
"""Validate that the value represents a MAC address
|
||||
|
||||
:param name: Name of the argument
|
||||
:param value: A string value representing a MAC address
|
||||
:returns: The value as a normalized MAC address, or None if value is None
|
||||
:raises: InvalidParameterValue if the value is not a valid MAC address
|
||||
"""
|
||||
if value is None:
|
||||
return
|
||||
try:
|
||||
return utils.validate_and_normalize_mac(value)
|
||||
except exception.InvalidMAC:
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Expected valid MAC address for %s: %s') % (name, value))
|
||||
|
||||
|
||||
def _or(name, value, validators):
|
||||
last_error = None
|
||||
for v in validators:
|
||||
try:
|
||||
return v(name=name, value=value)
|
||||
except exception.Invalid as e:
|
||||
last_error = e
|
||||
if last_error:
|
||||
raise last_error
|
||||
|
||||
|
||||
def or_valid(*validators):
|
||||
"""Validates if at least one supplied validator passes
|
||||
|
||||
:param name: Name of the argument
|
||||
:param value: A value
|
||||
:returns: The value returned from the first successful validator
|
||||
:raises: The error from the last validator when
|
||||
every validation fails
|
||||
"""
|
||||
assert validators, 'No validators specified for or_valid'
|
||||
return functools.partial(_or, validators=validators)
|
||||
|
||||
|
||||
def _and(name, value, validators):
|
||||
for v in validators:
|
||||
value = v(name=name, value=value)
|
||||
return value
|
||||
|
||||
|
||||
def and_valid(*validators):
|
||||
"""Validates that every supplied validator passes
|
||||
|
||||
The value returned from each validator is passed as the value to the next
|
||||
one.
|
||||
|
||||
:param name: Name of the argument
|
||||
:param value: A value
|
||||
:returns: The value transformed through every supplied validator
|
||||
:raises: The error from the first failed validator
|
||||
"""
|
||||
assert validators, 'No validators specified for or_valid'
|
||||
return functools.partial(_and, validators=validators)
|
||||
|
||||
|
||||
def _validate_schema(name, value, schema):
|
||||
if value is None:
|
||||
return
|
||||
try:
|
||||
jsonschema.validate(value, schema)
|
||||
except jsonschema.exceptions.ValidationError as e:
|
||||
|
||||
# The error message includes the whole schema which can be very
|
||||
# large and unhelpful, so truncate it to be brief and useful
|
||||
error_msg = ' '.join(str(e).split("\n")[:3])[:-1]
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Schema error for %s: %s') % (name, error_msg))
|
||||
return value
|
||||
|
||||
|
||||
def schema(schema):
|
||||
"""Return a validator function which validates the value with jsonschema
|
||||
|
||||
:param: schema dict representing jsonschema to validate with
|
||||
:returns: validator function which takes name and value arguments
|
||||
"""
|
||||
jsonschema.Draft4Validator.check_schema(schema)
|
||||
|
||||
return functools.partial(_validate_schema, schema=schema)
|
||||
|
||||
|
||||
def _validate_dict(name, value, validators):
|
||||
if value is None:
|
||||
return
|
||||
_validate_types(name, value, (dict, ))
|
||||
|
||||
for k, v in validators.items():
|
||||
if k in value:
|
||||
value[k] = v(name=k, value=value[k])
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def dict_valid(**validators):
|
||||
"""Return a validator function which validates dict fields
|
||||
|
||||
Validators will replace the value with the validation result. Any dict
|
||||
item which has no validator is ignored. When a key is missing in the value
|
||||
then the corresponding validator will not be run.
|
||||
|
||||
:param: validators dict where the key is a dict key to validate and the
|
||||
value is a validator function to run on that value
|
||||
:returns: validator function which takes name and value arguments
|
||||
"""
|
||||
return functools.partial(_validate_dict, validators=validators)
|
||||
|
||||
|
||||
def _validate_types(name, value, types):
|
||||
if not isinstance(value, types):
|
||||
str_types = ', '.join([str(t) for t in types])
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Expected types %s for %s: %s') % (str_types, name, value))
|
||||
return value
|
||||
|
||||
|
||||
def types(*types):
|
||||
"""Return a validator function which checks the value is one of the types
|
||||
|
||||
:param: types one or more types to use for the isinstance test
|
||||
:returns: validator function which takes name and value arguments
|
||||
"""
|
||||
return functools.partial(_validate_types, types=tuple(types))
|
||||
|
||||
|
||||
def _apply_validator(name, value, val_functions):
|
||||
if callable(val_functions):
|
||||
return val_functions(name, value)
|
||||
|
||||
for v in val_functions:
|
||||
value = v(name, value)
|
||||
return value
|
||||
|
||||
|
||||
def _inspect(function):
|
||||
sig = inspect.signature(function)
|
||||
param_keyword = None # **kwargs parameter
|
||||
param_positional = None # *args parameter
|
||||
params = []
|
||||
|
||||
for param in sig.parameters.values():
|
||||
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
||||
params.append(param)
|
||||
elif param.kind == inspect.Parameter.VAR_KEYWORD:
|
||||
param_keyword = param
|
||||
elif param.kind == inspect.Parameter.VAR_POSITIONAL:
|
||||
param_positional = param
|
||||
else:
|
||||
assert False, 'Unsupported parameter kind %s %s' % (
|
||||
param.name, param.kind
|
||||
)
|
||||
return params, param_positional, param_keyword
|
||||
|
||||
|
||||
def validate(*args, **kwargs):
|
||||
"""Decorator which validates and transforms function arguments
|
||||
|
||||
"""
|
||||
assert not args, 'Validators must be specifed by argument name'
|
||||
assert kwargs, 'No validators specified'
|
||||
validators = kwargs
|
||||
|
||||
def inner_function(function):
|
||||
params, param_positional, param_keyword = _inspect(function)
|
||||
|
||||
@functools.wraps(function)
|
||||
def inner_check_args(*args, **kwargs):
|
||||
args = list(args)
|
||||
args_len = len(args)
|
||||
kwargs_next = {}
|
||||
next_arg_index = 0
|
||||
|
||||
if not param_keyword:
|
||||
# ensure each named argument belongs to a param
|
||||
kwarg_keys = set(kwargs)
|
||||
param_names = set(p.name for p in params)
|
||||
extra_args = kwarg_keys.difference(param_names)
|
||||
if extra_args:
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Unexpected arguments: %s') % ', '.join(extra_args))
|
||||
|
||||
for i, param in enumerate(params):
|
||||
|
||||
if i == 0 and param.name == 'self':
|
||||
# skip validating self
|
||||
continue
|
||||
|
||||
val_function = validators.get(param.name)
|
||||
if not val_function:
|
||||
continue
|
||||
|
||||
if i < args_len:
|
||||
# validate positional argument
|
||||
args[i] = val_function(param.name, args[i])
|
||||
next_arg_index = i + 1
|
||||
|
||||
elif param.name in kwargs:
|
||||
# validate keyword argument
|
||||
kwargs_next[param.name] = val_function(
|
||||
param.name, kwargs.pop(param.name))
|
||||
elif param.default == inspect.Parameter.empty:
|
||||
# no argument was provided, and there is no default
|
||||
# in the parameter, so this is a mandatory argument
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Missing mandatory parameter: %s') % param.name)
|
||||
|
||||
if param_positional:
|
||||
# handle validating *args
|
||||
val_function = validators.get(param_positional.name)
|
||||
remaining = args[next_arg_index:]
|
||||
if val_function and remaining:
|
||||
args = args[:next_arg_index]
|
||||
args.extend(val_function(param_positional.name, remaining))
|
||||
|
||||
# handle validating remaining **kwargs
|
||||
if kwargs:
|
||||
val_function = (param_keyword
|
||||
and validators.get(param_keyword.name))
|
||||
if val_function:
|
||||
kwargs_next.update(
|
||||
val_function(param_keyword.name, kwargs))
|
||||
else:
|
||||
# make sure unvalidated keyword arguments are kept
|
||||
kwargs_next.update(kwargs)
|
||||
|
||||
return function(*args, **kwargs_next)
|
||||
return inner_check_args
|
||||
return inner_function
|
||||
|
||||
|
||||
patch = schema({
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'path': {'type': 'string', 'pattern': '^(/[\\w-]+)+$'},
|
||||
'op': {'type': 'string', 'enum': ['add', 'replace', 'remove']},
|
||||
'value': {}
|
||||
},
|
||||
'additionalProperties': False
|
||||
}
|
||||
})
|
||||
"""Validate a patch API operation"""
|
|
@ -0,0 +1,618 @@
|
|||
# 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.
|
||||
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from ironic.common import args
|
||||
from ironic.common import exception
|
||||
from ironic.tests import base
|
||||
|
||||
|
||||
class ArgsDecorated(object):
|
||||
|
||||
@args.validate(one=args.string,
|
||||
two=args.boolean,
|
||||
three=args.uuid,
|
||||
four=args.uuid_or_name)
|
||||
def method(self, one, two, three, four):
|
||||
return one, two, three, four
|
||||
|
||||
@args.validate(one=args.string)
|
||||
def needs_string(self, one):
|
||||
return one
|
||||
|
||||
@args.validate(one=args.boolean)
|
||||
def needs_boolean(self, one):
|
||||
return one
|
||||
|
||||
@args.validate(one=args.uuid)
|
||||
def needs_uuid(self, one):
|
||||
return one
|
||||
|
||||
@args.validate(one=args.name)
|
||||
def needs_name(self, one):
|
||||
return one
|
||||
|
||||
@args.validate(one=args.uuid_or_name)
|
||||
def needs_uuid_or_name(self, one):
|
||||
return one
|
||||
|
||||
@args.validate(one=args.string_list)
|
||||
def needs_string_list(self, one):
|
||||
return one
|
||||
|
||||
@args.validate(one=args.integer)
|
||||
def needs_integer(self, one):
|
||||
return one
|
||||
|
||||
@args.validate(one=args.mac_address)
|
||||
def needs_mac_address(self, one):
|
||||
return one
|
||||
|
||||
@args.validate(one=args.schema({
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': {'type': 'string'},
|
||||
'count': {'type': 'integer', 'minimum': 0},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
'required': ['name'],
|
||||
}
|
||||
}))
|
||||
def needs_schema(self, one):
|
||||
return one
|
||||
|
||||
@args.validate(one=args.string, two=args.string, the_rest=args.schema({
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'three': {'type': 'string'},
|
||||
'four': {'type': 'string', 'maxLength': 4},
|
||||
'five': {'type': 'string'},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
'required': ['three']
|
||||
}))
|
||||
def needs_schema_kwargs(self, one, two, **the_rest):
|
||||
return one, two, the_rest
|
||||
|
||||
@args.validate(one=args.string, two=args.string, the_rest=args.schema({
|
||||
'type': 'array',
|
||||
'items': {'type': 'string'}
|
||||
}))
|
||||
def needs_schema_args(self, one, two=None, *the_rest):
|
||||
return one, two, the_rest
|
||||
|
||||
@args.validate(one=args.string, two=args.string, args=args.schema({
|
||||
'type': 'array',
|
||||
'items': {'type': 'string'}
|
||||
}), kwargs=args.schema({
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'four': {'type': 'string'},
|
||||
},
|
||||
}))
|
||||
def needs_schema_mixed(self, one, two=None, *args, **kwargs):
|
||||
return one, two, args, kwargs
|
||||
|
||||
@args.validate(one=args.string)
|
||||
def needs_mixed_unvalidated(self, one, two=None, *args, **kwargs):
|
||||
return one, two, args, kwargs
|
||||
|
||||
@args.validate(body=args.patch)
|
||||
def patch(self, body):
|
||||
return body
|
||||
|
||||
|
||||
class BaseTest(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(BaseTest, self).setUp()
|
||||
self.decorated = ArgsDecorated()
|
||||
|
||||
|
||||
class ValidateDecoratorTest(BaseTest):
|
||||
|
||||
def test_decorated_args(self):
|
||||
uuid = uuidutils.generate_uuid()
|
||||
self.assertEqual((
|
||||
'a',
|
||||
True,
|
||||
uuid,
|
||||
'a_name',
|
||||
), self.decorated.method(
|
||||
'a',
|
||||
True,
|
||||
uuid,
|
||||
'a_name',
|
||||
))
|
||||
|
||||
def test_decorated_kwargs(self):
|
||||
uuid = uuidutils.generate_uuid()
|
||||
self.assertEqual((
|
||||
'a',
|
||||
True,
|
||||
uuid,
|
||||
'a_name',
|
||||
), self.decorated.method(
|
||||
one='a',
|
||||
two=True,
|
||||
three=uuid,
|
||||
four='a_name',
|
||||
))
|
||||
|
||||
def test_decorated_args_kwargs(self):
|
||||
uuid = uuidutils.generate_uuid()
|
||||
self.assertEqual((
|
||||
'a',
|
||||
True,
|
||||
uuid,
|
||||
'a_name',
|
||||
), self.decorated.method(
|
||||
'a',
|
||||
True,
|
||||
uuid,
|
||||
four='a_name',
|
||||
))
|
||||
|
||||
def test_decorated_function(self):
|
||||
|
||||
@args.validate(one=args.string,
|
||||
two=args.boolean,
|
||||
three=args.uuid,
|
||||
four=args.uuid_or_name)
|
||||
def func(one, two, three, four):
|
||||
return one, two, three, four
|
||||
|
||||
uuid = uuidutils.generate_uuid()
|
||||
self.assertEqual((
|
||||
'a',
|
||||
True,
|
||||
uuid,
|
||||
'a_name',
|
||||
), func(
|
||||
'a',
|
||||
'yes',
|
||||
uuid,
|
||||
four='a_name',
|
||||
))
|
||||
|
||||
def test_unexpected_args(self):
|
||||
uuid = uuidutils.generate_uuid()
|
||||
e = self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
self.decorated.method,
|
||||
one='a',
|
||||
two=True,
|
||||
three=uuid,
|
||||
four='a_name',
|
||||
five='5',
|
||||
six=6
|
||||
)
|
||||
self.assertIn('Unexpected arguments: ', str(e))
|
||||
self.assertIn('five', str(e))
|
||||
self.assertIn('six', str(e))
|
||||
|
||||
def test_string(self):
|
||||
self.assertEqual('foo', self.decorated.needs_string('foo'))
|
||||
self.assertIsNone(self.decorated.needs_string(None))
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_string, 123)
|
||||
|
||||
def test_boolean(self):
|
||||
self.assertTrue(self.decorated.needs_boolean('yes'))
|
||||
self.assertTrue(self.decorated.needs_boolean('true'))
|
||||
self.assertTrue(self.decorated.needs_boolean(True))
|
||||
|
||||
self.assertFalse(self.decorated.needs_boolean('no'))
|
||||
self.assertFalse(self.decorated.needs_boolean('false'))
|
||||
self.assertFalse(self.decorated.needs_boolean(False))
|
||||
|
||||
self.assertIsNone(self.decorated.needs_boolean(None))
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_boolean,
|
||||
'yeah nah yeah nah')
|
||||
|
||||
def test_uuid(self):
|
||||
uuid = uuidutils.generate_uuid()
|
||||
self.assertEqual(uuid, self.decorated.needs_uuid(uuid))
|
||||
self.assertIsNone(self.decorated.needs_uuid(None))
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_uuid, uuid + 'XXX')
|
||||
|
||||
def test_name(self):
|
||||
self.assertEqual('foo', self.decorated.needs_name('foo'))
|
||||
self.assertIsNone(self.decorated.needs_name(None))
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_name, 'I am a name')
|
||||
|
||||
def test_uuid_or_name(self):
|
||||
uuid = uuidutils.generate_uuid()
|
||||
self.assertEqual(uuid, self.decorated.needs_uuid_or_name(uuid))
|
||||
self.assertEqual('foo', self.decorated.needs_uuid_or_name('foo'))
|
||||
self.assertIsNone(self.decorated.needs_uuid_or_name(None))
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_uuid_or_name,
|
||||
'I am a name')
|
||||
|
||||
def test_string_list(self):
|
||||
self.assertEqual([
|
||||
'foo', 'bar', 'baz'
|
||||
], self.decorated.needs_string_list('foo, bar ,bAZ'))
|
||||
self.assertIsNone(self.decorated.needs_name(None))
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_name, True)
|
||||
|
||||
def test_integer(self):
|
||||
self.assertEqual(123, self.decorated.needs_integer(123))
|
||||
self.assertIsNone(self.decorated.needs_integer(None))
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_integer,
|
||||
'more than a number')
|
||||
|
||||
def test_mac_address(self):
|
||||
self.assertEqual('02:ce:20:50:68:6f',
|
||||
self.decorated.needs_mac_address('02:cE:20:50:68:6F'))
|
||||
self.assertIsNone(self.decorated.needs_mac_address(None))
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_mac_address,
|
||||
'big:mac')
|
||||
|
||||
def test_mixed_unvalidated(self):
|
||||
# valid
|
||||
self.assertEqual((
|
||||
'one', 'two', ('three', 'four', 'five'), {}
|
||||
), self.decorated.needs_mixed_unvalidated(
|
||||
'one', 'two', 'three', 'four', 'five',
|
||||
))
|
||||
self.assertEqual((
|
||||
'one', 'two', ('three',), {'four': 'four', 'five': 'five'}
|
||||
), self.decorated.needs_mixed_unvalidated(
|
||||
'one', 'two', 'three', four='four', five='five',
|
||||
))
|
||||
self.assertEqual((
|
||||
'one', 'two', (), {}
|
||||
), self.decorated.needs_mixed_unvalidated(
|
||||
'one', 'two',
|
||||
))
|
||||
self.assertEqual((
|
||||
'one', None, (), {}
|
||||
), self.decorated.needs_mixed_unvalidated(
|
||||
'one',
|
||||
))
|
||||
|
||||
# wrong type in one
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_mixed_unvalidated, 1)
|
||||
|
||||
def test_mandatory(self):
|
||||
|
||||
@args.validate(foo=args.string)
|
||||
def doit(foo):
|
||||
return foo
|
||||
|
||||
@args.validate(foo=args.string)
|
||||
def doit_maybe(foo='baz'):
|
||||
return foo
|
||||
|
||||
# valid
|
||||
self.assertEqual('bar', doit('bar'))
|
||||
|
||||
# invalid, argument not provided
|
||||
self.assertRaises(exception.InvalidParameterValue, doit)
|
||||
|
||||
# valid, not mandatory
|
||||
self.assertEqual('baz', doit_maybe())
|
||||
|
||||
def test_or(self):
|
||||
|
||||
@args.validate(foo=args.or_valid(
|
||||
args.string,
|
||||
args.integer,
|
||||
args.boolean
|
||||
))
|
||||
def doit(foo):
|
||||
return foo
|
||||
|
||||
# valid
|
||||
self.assertEqual('bar', doit('bar'))
|
||||
self.assertEqual(1, doit(1))
|
||||
self.assertEqual(True, doit(True))
|
||||
|
||||
# invalid, wrong type
|
||||
self.assertRaises(exception.InvalidParameterValue, doit, {})
|
||||
|
||||
def test_and(self):
|
||||
|
||||
@args.validate(foo=args.and_valid(
|
||||
args.string,
|
||||
args.name
|
||||
))
|
||||
def doit(foo):
|
||||
return foo
|
||||
|
||||
# valid
|
||||
self.assertEqual('bar', doit('bar'))
|
||||
|
||||
# invalid, not a string
|
||||
self.assertRaises(exception.InvalidParameterValue, doit, 2)
|
||||
|
||||
# invalid, not a name
|
||||
self.assertRaises(exception.InvalidParameterValue, doit, 'not a name')
|
||||
|
||||
|
||||
class ValidateSchemaTest(BaseTest):
|
||||
|
||||
def test_schema(self):
|
||||
valid = [
|
||||
{'name': 'zero'},
|
||||
{'name': 'one', 'count': 1},
|
||||
{'name': 'two', 'count': 2}
|
||||
]
|
||||
invalid_count = [
|
||||
{'name': 'neg', 'count': -1},
|
||||
{'name': 'one', 'count': 1},
|
||||
{'name': 'two', 'count': 2}
|
||||
]
|
||||
invalid_root = {}
|
||||
self.assertEqual(valid, self.decorated.needs_schema(valid))
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_schema,
|
||||
invalid_count)
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_schema,
|
||||
invalid_root)
|
||||
|
||||
def test_schema_needs_kwargs(self):
|
||||
# valid
|
||||
self.assertEqual((
|
||||
'one', 'two', {
|
||||
'three': 'three',
|
||||
'four': 'four',
|
||||
'five': 'five',
|
||||
}
|
||||
), self.decorated.needs_schema_kwargs(
|
||||
one='one',
|
||||
two='two',
|
||||
three='three',
|
||||
four='four',
|
||||
five='five',
|
||||
))
|
||||
self.assertEqual((
|
||||
'one', 'two', {
|
||||
'three': 'three',
|
||||
}
|
||||
), self.decorated.needs_schema_kwargs(
|
||||
one='one',
|
||||
two='two',
|
||||
three='three',
|
||||
))
|
||||
self.assertEqual((
|
||||
'one', 'two', {}
|
||||
), self.decorated.needs_schema_kwargs(
|
||||
one='one',
|
||||
two='two',
|
||||
))
|
||||
|
||||
# missing mandatory 'three'
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_schema_kwargs,
|
||||
one='one', two='two', four='four', five='five')
|
||||
|
||||
# 'four' value exceeds length
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_schema_kwargs,
|
||||
one='one', two='two', three='three',
|
||||
four='beforefore', five='five')
|
||||
|
||||
def test_schema_needs_args(self):
|
||||
# valid
|
||||
self.assertEqual((
|
||||
'one', 'two', ('three', 'four', 'five')
|
||||
), self.decorated.needs_schema_args(
|
||||
'one', 'two', 'three', 'four', 'five',
|
||||
))
|
||||
self.assertEqual((
|
||||
'one', 'two', ('three',)
|
||||
), self.decorated.needs_schema_args(
|
||||
'one', 'two', 'three',
|
||||
))
|
||||
self.assertEqual((
|
||||
'one', 'two', ()
|
||||
), self.decorated.needs_schema_args(
|
||||
'one', 'two',
|
||||
))
|
||||
self.assertEqual((
|
||||
'one', None, ()
|
||||
), self.decorated.needs_schema_args(
|
||||
'one',
|
||||
))
|
||||
|
||||
# failed, non string *the_rest value
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_schema_args,
|
||||
'one', 'two', 'three', 4, False)
|
||||
|
||||
def test_schema_needs_mixed(self):
|
||||
# valid
|
||||
self.assertEqual((
|
||||
'one', 'two', ('three', 'four', 'five'), {}
|
||||
), self.decorated.needs_schema_mixed(
|
||||
'one', 'two', 'three', 'four', 'five',
|
||||
))
|
||||
self.assertEqual((
|
||||
'one', 'two', ('three', ), {'four': 'four'}
|
||||
), self.decorated.needs_schema_mixed(
|
||||
'one', 'two', 'three', four='four',
|
||||
))
|
||||
self.assertEqual((
|
||||
'one', 'two', (), {'four': 'four'}
|
||||
), self.decorated.needs_schema_mixed(
|
||||
'one', 'two', four='four',
|
||||
))
|
||||
self.assertEqual((
|
||||
'one', None, (), {}
|
||||
), self.decorated.needs_schema_mixed(
|
||||
'one',
|
||||
))
|
||||
|
||||
# wrong type in *args
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_schema_mixed,
|
||||
'one', 'two', 3, four='four')
|
||||
# wrong type in *kwargs
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.needs_schema_mixed,
|
||||
'one', 'two', 'three', four=4)
|
||||
|
||||
|
||||
class ValidatePatchSchemaTest(BaseTest):
|
||||
|
||||
def test_patch(self):
|
||||
data = [{
|
||||
'path': '/foo',
|
||||
'op': 'replace',
|
||||
'value': 'bar'
|
||||
}, {
|
||||
'path': '/foo/bar',
|
||||
'op': 'add',
|
||||
'value': True
|
||||
}, {
|
||||
'path': '/foo/bar/baz',
|
||||
'op': 'remove',
|
||||
'value': 123
|
||||
}]
|
||||
|
||||
self.assertEqual(
|
||||
data,
|
||||
self.decorated.patch(data)
|
||||
)
|
||||
|
||||
def assertValidationFailed(self, data, error_snippets=None):
|
||||
e = self.assertRaises(exception.InvalidParameterValue,
|
||||
self.decorated.patch, data)
|
||||
if error_snippets:
|
||||
for s in error_snippets:
|
||||
self.assertIn(s, str(e))
|
||||
|
||||
def test_patch_validation_failed(self):
|
||||
self.assertValidationFailed(
|
||||
{},
|
||||
["Schema error for body:",
|
||||
"{} is not of type 'array'"])
|
||||
self.assertValidationFailed(
|
||||
[{
|
||||
'path': '/foo/bar/baz',
|
||||
'op': 'fribble',
|
||||
'value': 123
|
||||
}],
|
||||
["Schema error for body:",
|
||||
"'fribble' is not one of ['add', 'replace', 'remove']"])
|
||||
self.assertValidationFailed(
|
||||
[{
|
||||
'path': '/',
|
||||
'op': 'add',
|
||||
'value': 123
|
||||
}],
|
||||
["Schema error for body:",
|
||||
"'/' does not match"])
|
||||
self.assertValidationFailed(
|
||||
[{
|
||||
'path': 'foo/',
|
||||
'op': 'add',
|
||||
'value': 123
|
||||
}],
|
||||
["Schema error for body:",
|
||||
"'foo/' does not match"])
|
||||
self.assertValidationFailed(
|
||||
[{
|
||||
'path': '/foo bar',
|
||||
'op': 'add',
|
||||
'value': 123
|
||||
}],
|
||||
["Schema error for body:",
|
||||
"'/foo bar' does not match"])
|
||||
|
||||
|
||||
class ValidateDictTest(BaseTest):
|
||||
|
||||
def test_dict_valid(self):
|
||||
uuid = uuidutils.generate_uuid()
|
||||
|
||||
@args.validate(foo=args.dict_valid(
|
||||
bar=args.uuid
|
||||
))
|
||||
def doit(foo):
|
||||
return foo
|
||||
|
||||
# validate passes
|
||||
doit(foo={'bar': uuid})
|
||||
|
||||
# tolerate other keys
|
||||
doit(foo={'bar': uuid, 'baz': 'baz'})
|
||||
|
||||
# key missing
|
||||
doit({})
|
||||
|
||||
# value fails validation
|
||||
e = self.assertRaises(exception.InvalidParameterValue,
|
||||
doit, {'bar': uuid + 'XXX'})
|
||||
self.assertIn('Expected UUID for bar:', str(e))
|
||||
|
||||
# not a dict
|
||||
e = self.assertRaises(exception.InvalidParameterValue,
|
||||
doit, 'asdf')
|
||||
self.assertIn("Expected types <class 'dict'> for foo: asdf", str(e))
|
||||
|
||||
def test_dict_valid_colon_key_name(self):
|
||||
uuid = uuidutils.generate_uuid()
|
||||
|
||||
@args.validate(foo=args.dict_valid(**{
|
||||
'bar:baz': args.uuid
|
||||
}
|
||||
))
|
||||
def doit(foo):
|
||||
return foo
|
||||
|
||||
# validate passes
|
||||
doit(foo={'bar:baz': uuid})
|
||||
|
||||
# value fails validation
|
||||
e = self.assertRaises(exception.InvalidParameterValue,
|
||||
doit, {'bar:baz': uuid + 'XXX'})
|
||||
self.assertIn('Expected UUID for bar:', str(e))
|
||||
|
||||
|
||||
class ValidateTypesTest(BaseTest):
|
||||
|
||||
def test_types(self):
|
||||
|
||||
@args.validate(foo=args.types(type(None), dict, str))
|
||||
def doit(foo):
|
||||
return foo
|
||||
|
||||
# valid None
|
||||
self.assertIsNone(doit(None))
|
||||
|
||||
# valid dict
|
||||
self.assertEqual({'foo': 'bar'}, doit({'foo': 'bar'}))
|
||||
|
||||
# valid string
|
||||
self.assertEqual('foo', doit('foo'))
|
||||
|
||||
# invalid integer
|
||||
e = self.assertRaises(exception.InvalidParameterValue,
|
||||
doit, 123)
|
||||
self.assertIn("Expected types "
|
||||
"<class 'NoneType'>, <class 'dict'>, <class 'str'> "
|
||||
"for foo: 123", str(e))
|
Loading…
Reference in New Issue