560 lines
21 KiB
Python
560 lines
21 KiB
Python
# Copyright 2016 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 fixtures
|
|
from jsonschema import exceptions as jsonschema_exc
|
|
import six
|
|
from six.moves import http_client as http
|
|
import sys
|
|
|
|
from masakari.api import api_version_request as api_version
|
|
from masakari.api import validation
|
|
from masakari.api.validation import parameter_types
|
|
from masakari.api.validation import validators
|
|
from masakari import exception
|
|
from masakari import test
|
|
|
|
|
|
class FakeRequest(object):
|
|
api_version_request = api_version.APIVersionRequest("1.0")
|
|
environ = {}
|
|
|
|
|
|
class ValidationRegex(test.NoDBTestCase):
|
|
|
|
def test_build_regex_range(self):
|
|
|
|
def _get_all_chars():
|
|
for i in range(0x7F):
|
|
yield six.unichr(i)
|
|
|
|
self.useFixture(fixtures.MonkeyPatch(
|
|
'masakari.api.validation.parameter_types._get_all_chars',
|
|
_get_all_chars))
|
|
|
|
r = parameter_types._build_regex_range(ws=False)
|
|
self.assertEqual(r, re.escape('!') + '-' + re.escape('~'))
|
|
|
|
# if we allow whitespace the range starts earlier
|
|
r = parameter_types._build_regex_range(ws=True)
|
|
self.assertEqual(r, re.escape(' ') + '-' + re.escape('~'))
|
|
|
|
# excluding a character will give us 2 ranges
|
|
r = parameter_types._build_regex_range(ws=True, exclude=['A'])
|
|
self.assertEqual(r,
|
|
re.escape(' ') + '-' + re.escape('@') +
|
|
'B' + '-' + re.escape('~'))
|
|
|
|
# inverting which gives us all the initial unprintable characters.
|
|
r = parameter_types._build_regex_range(ws=False, invert=True)
|
|
self.assertEqual(r,
|
|
re.escape('\x00') + '-' + re.escape(' '))
|
|
|
|
# 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(r,
|
|
re.escape(' ') + '-' + re.escape('@') +
|
|
'B' + 'D' + '-' + re.escape('~'))
|
|
|
|
# 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(r,
|
|
re.escape('\x00') + '-' + re.escape('\x1f') + 'A-CZ')
|
|
|
|
|
|
class APIValidationTestCase(test.NoDBTestCase):
|
|
|
|
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 does not happen.')
|
|
|
|
|
|
class FormatCheckerTestCase(test.NoDBTestCase):
|
|
|
|
def test_format_checker_failed(self):
|
|
format_checker = validators.FormatChecker()
|
|
exc = self.assertRaises(jsonschema_exc.FormatError,
|
|
format_checker.check, " ", "name")
|
|
self.assertIsInstance(exc.cause, exception.InvalidName)
|
|
self.assertEqual("An invalid 'name' value was provided. The name must "
|
|
"be: printable characters. "
|
|
"Can not start or end with whitespace.",
|
|
exc.cause.format_message())
|
|
|
|
def test_format_checker_failed_with_non_string(self):
|
|
checks = ["name"]
|
|
format_checker = validators.FormatChecker()
|
|
|
|
for check in checks:
|
|
exc = self.assertRaises(jsonschema_exc.FormatError,
|
|
format_checker.check, None, "name")
|
|
self.assertIsInstance(exc.cause, exception.InvalidName)
|
|
self.assertEqual("An invalid 'name' value was provided. The name "
|
|
"must be: printable characters. "
|
|
"Can not start or end with whitespace.",
|
|
exc.cause.format_message())
|
|
|
|
|
|
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(self.post(body={'foo': 1}, req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
self.assertEqual(self.post(body={'abc': 1}, req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
|
|
|
|
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(self.post(body={'foo': 1},
|
|
req=FakeRequest()), 'Validation succeeded.')
|
|
|
|
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(self.post(body={'foo': 1}, req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
self.assertEqual(self.post(body={'foo': 1, 'ext': 1},
|
|
req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
|
|
|
|
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(self.post(body={'foo': 1}, req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
|
|
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(self.post(body={'foo': 'abc'}, req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
self.assertEqual(self.post(body={'foo': '0'}, req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
self.assertEqual(self.post(body={'foo': ''}, req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
|
|
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(self.post(body={'foo': '0'}, req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
self.assertEqual(self.post(body={'foo': '0123456789'},
|
|
req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
|
|
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(self.post(body={'foo': 1}, req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
self.assertEqual(self.post(body={'foo': '1'}, req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
self.assertEqual(self.post(body={'foo': '0123456789'},
|
|
req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
|
|
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(self.post(body={'foo': 1}, req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
self.assertEqual(self.post(body={'foo': 10}, req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
self.assertEqual(self.post(body={'foo': '1'}, req=FakeRequest()),
|
|
'Validation succeeded.')
|
|
|
|
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',"
|
|
" False, 'False', 'FALSE', 'false', '0', 'OFF', 'Off',"
|
|
" 'off', 'NO', 'No', 'no']")
|
|
|
|
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': 'm1.small'},
|
|
req=FakeRequest()))
|
|
self.assertEqual('Validation succeeded.',
|
|
self.post(body={'foo': 'my server'},
|
|
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()))
|
|
|
|
def test_validate_name_fails(self):
|
|
error = ("An invalid 'name' value was provided. The name must be: "
|
|
"printable characters. "
|
|
"Can not start or end with whitespace.")
|
|
|
|
should_fail = (' ',
|
|
' segment',
|
|
'segment ',
|
|
u'a\xa0', # trailing unicode space
|
|
u'\uffff', # non-printable unicode
|
|
)
|
|
|
|
for item in should_fail:
|
|
self.check_validation_error(self.post, body={'foo': item},
|
|
expected_detail=error)
|
|
|
|
# four-byte unicode, if supported by this python build
|
|
try:
|
|
self.check_validation_error(self.post, body={'foo': u'\U00010000'},
|
|
expected_detail=error)
|
|
except ValueError:
|
|
pass
|
|
|
|
|
|
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': '2016-01-14T01:00:00Z'}, req=FakeRequest()
|
|
))
|