Merge "Add API Validation framework for flavors"
This commit is contained in:
commit
36aabb9011
@ -5,6 +5,7 @@ Flask==0.11.1
|
||||
Flask-Cors==3.0.2
|
||||
Flask-RESTful==0.3.5
|
||||
itsdangerous==0.24
|
||||
jsonschema>=2.0.0,<3.0.0,!=2.5.0 # MIT
|
||||
Jinja2==2.8
|
||||
MarkupSafe==0.23
|
||||
python-dateutil==2.5.3
|
||||
|
@ -20,6 +20,7 @@ from six.moves import http_client
|
||||
|
||||
from valence.common import utils
|
||||
from valence.controller import flavors
|
||||
from valence.validation import validator
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@ -29,6 +30,7 @@ class Flavors(Resource):
|
||||
def get(self):
|
||||
return utils.make_response(http_client.OK, flavors.list_flavors())
|
||||
|
||||
@validator.check_input('flavor_schema')
|
||||
def post(self):
|
||||
return utils.make_response(http_client.OK,
|
||||
flavors.create_flavor(request.get_json()))
|
||||
|
@ -99,14 +99,21 @@ class NotFound(ValenceError):
|
||||
|
||||
class BadRequest(ValenceError):
|
||||
|
||||
def __init__(self, detail='bad request', request_id=FAKE_REQUEST_ID):
|
||||
def __init__(self, detail='bad request', request_id=FAKE_REQUEST_ID,
|
||||
code=None):
|
||||
self.request_id = request_id
|
||||
self.status = http_client.BAD_REQUEST
|
||||
self.code = "BadRequest"
|
||||
self.code = code or "BadRequest"
|
||||
self.title = "Malformed or Missing Payload in Request"
|
||||
self.detail = detail
|
||||
|
||||
|
||||
class ValidationError(BadRequest):
|
||||
def __init__(self, detail='Validation Error', request_id=None):
|
||||
super(ValidationError, self).__init__(detail=detail,
|
||||
code='ValidationError')
|
||||
|
||||
|
||||
def _error(error_code, http_status, error_title, error_detail,
|
||||
request_id=FAKE_REQUEST_ID):
|
||||
# responseobj - the response object of Requests framework
|
||||
|
0
valence/tests/unit/validation/__init__.py
Normal file
0
valence/tests/unit/validation/__init__.py
Normal file
50
valence/tests/unit/validation/test_validation.py
Normal file
50
valence/tests/unit/validation/test_validation.py
Normal file
@ -0,0 +1,50 @@
|
||||
import json
|
||||
|
||||
import mock
|
||||
from oslotest import base
|
||||
|
||||
from valence.api import app as flask_app
|
||||
from valence.tests.unit.fakes import flavor_fakes
|
||||
|
||||
|
||||
class TestApiValidation(base.BaseTestCase):
|
||||
"""Test case base class for all unit tests."""
|
||||
|
||||
def setUp(self):
|
||||
super(TestApiValidation, self).setUp()
|
||||
app = flask_app.get_app()
|
||||
app.config['TESTING'] = True
|
||||
self.app = app.test_client()
|
||||
self.flavor = flavor_fakes.fake_flavor()
|
||||
|
||||
@mock.patch('valence.controller.flavors.create_flavor')
|
||||
def test_flavor_create(self, mock_create):
|
||||
flavor = self.flavor
|
||||
flavor.pop('uuid')
|
||||
mock_create.return_value = self.flavor
|
||||
response = self.app.post('/v1/flavors',
|
||||
content_type='application/json',
|
||||
data=json.dumps(flavor))
|
||||
self.assertEqual(200, response.status_code)
|
||||
mock_create.assert_called_once_with(flavor)
|
||||
|
||||
def test_flavor_create_incorrect_param(self):
|
||||
flavor = self.flavor
|
||||
flavor.pop('uuid')
|
||||
# Test invalid value
|
||||
flavor['properties']['memory']['capacity_mib'] = 10
|
||||
response = self.app.post('/v1/flavors',
|
||||
content_type='application/json',
|
||||
data=json.dumps(self.flavor))
|
||||
response = json.loads(response.data.decode())
|
||||
self.assertEqual(400, response['status'])
|
||||
self.assertEqual('ValidationError', response['code'])
|
||||
|
||||
# Test invalid key
|
||||
flavor['properties']['invalid_key'] = 'invalid'
|
||||
response = self.app.post('/v1/flavors',
|
||||
content_type='application/json',
|
||||
data=json.dumps(self.flavor))
|
||||
response = json.loads(response.data.decode())
|
||||
self.assertEqual(400, response['status'])
|
||||
self.assertEqual('ValidationError', response['code'])
|
0
valence/validation/__init__.py
Normal file
0
valence/validation/__init__.py
Normal file
37
valence/validation/schemas.py
Normal file
37
valence/validation/schemas.py
Normal file
@ -0,0 +1,37 @@
|
||||
import jsonschema
|
||||
|
||||
flavor_schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': {'type': 'string'},
|
||||
'description': {'type': 'string'},
|
||||
'properties': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'memory': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'capacity_mib': {'type': 'string'},
|
||||
'type': {'type': 'string'}
|
||||
},
|
||||
'additionalProperties': False,
|
||||
},
|
||||
'processor': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'total_cores': {'type': 'string'},
|
||||
'model': {'type': 'string'},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
},
|
||||
},
|
||||
'required': ['name', 'properties'],
|
||||
'additionalProperties': False,
|
||||
}
|
||||
|
||||
|
||||
jsonschema.Draft4Validator.check_schema(flavor_schema)
|
||||
SCHEMAS = {'flavor_schema': flavor_schema}
|
53
valence/validation/validator.py
Normal file
53
valence/validation/validator.py
Normal file
@ -0,0 +1,53 @@
|
||||
# Copyright (c) 2017 NEC, Corp.
|
||||
#
|
||||
# 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 logging
|
||||
|
||||
from flask import request
|
||||
import jsonschema
|
||||
|
||||
from valence.common import exception
|
||||
from valence.validation import schemas
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_input(schema_name):
|
||||
def decorated(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
data = request.get_json()
|
||||
LOG.debug("validating input %s with schema %s", data, schema_name)
|
||||
schema_validator = Validator(schema_name)
|
||||
schema_validator.validate(data)
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
return decorated
|
||||
|
||||
|
||||
class Validator(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.schema = schemas.SCHEMAS.get(name)
|
||||
checker = jsonschema.FormatChecker()
|
||||
self.validator = jsonschema.Draft4Validator(self.schema,
|
||||
format_checker=checker)
|
||||
|
||||
def validate(self, data):
|
||||
try:
|
||||
self.validator.validate(data)
|
||||
except jsonschema.ValidationError as e:
|
||||
LOG.exception("Failed to validate the input")
|
||||
raise exception.ValidationError(detail=e.message)
|
Loading…
x
Reference in New Issue
Block a user