summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSteve Baker <sbaker@redhat.com>2020-07-24 17:12:55 +1200
committerSteve Baker <sbaker@redhat.com>2020-11-16 10:39:36 +1300
commit6ea0e8eaf65330e47e9be231fd7871474ad16f76 (patch)
treefd85ea4abdfb98fbc99656816076a3c5857f439e
parentb861ea022b21978e1fa0db7ef90b3cc56fc1c908 (diff)
downloadironic-6ea0e8eaf65330e47e9be231fd7871474ad16f76.tar.gz
New argument validate decorator
This will replace @expose.expose parameter validation of method arguments. This implementation is complete and every required validator has been implemented. Implementing *args and **kwargs support is probably overkill with the followup @expose.body decorator, but it might be nice to have in the future. Change-Id: I62492f7396805433ac3577274ffbbb5d787d0fc1 Story: 1651346 Task: 10551
-rwxr-xr-xironic/common/args.py394
-rw-r--r--ironic/tests/unit/common/test_args.py618
2 files changed, 1012 insertions, 0 deletions
diff --git a/ironic/common/args.py b/ironic/common/args.py
new file mode 100755
index 000000000..014fbd318
--- /dev/null
+++ b/ironic/common/args.py
@@ -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"""
diff --git a/ironic/tests/unit/common/test_args.py b/ironic/tests/unit/common/test_args.py
new file mode 100644
index 000000000..c4b4d2e88
--- /dev/null
+++ b/ironic/tests/unit/common/test_args.py
@@ -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))