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-Cors==3.0.2
|
||||||
Flask-RESTful==0.3.5
|
Flask-RESTful==0.3.5
|
||||||
itsdangerous==0.24
|
itsdangerous==0.24
|
||||||
|
jsonschema>=2.0.0,<3.0.0,!=2.5.0 # MIT
|
||||||
Jinja2==2.8
|
Jinja2==2.8
|
||||||
MarkupSafe==0.23
|
MarkupSafe==0.23
|
||||||
python-dateutil==2.5.3
|
python-dateutil==2.5.3
|
||||||
|
@ -20,6 +20,7 @@ from six.moves import http_client
|
|||||||
|
|
||||||
from valence.common import utils
|
from valence.common import utils
|
||||||
from valence.controller import flavors
|
from valence.controller import flavors
|
||||||
|
from valence.validation import validator
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -29,6 +30,7 @@ class Flavors(Resource):
|
|||||||
def get(self):
|
def get(self):
|
||||||
return utils.make_response(http_client.OK, flavors.list_flavors())
|
return utils.make_response(http_client.OK, flavors.list_flavors())
|
||||||
|
|
||||||
|
@validator.check_input('flavor_schema')
|
||||||
def post(self):
|
def post(self):
|
||||||
return utils.make_response(http_client.OK,
|
return utils.make_response(http_client.OK,
|
||||||
flavors.create_flavor(request.get_json()))
|
flavors.create_flavor(request.get_json()))
|
||||||
|
@ -99,14 +99,21 @@ class NotFound(ValenceError):
|
|||||||
|
|
||||||
class BadRequest(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.request_id = request_id
|
||||||
self.status = http_client.BAD_REQUEST
|
self.status = http_client.BAD_REQUEST
|
||||||
self.code = "BadRequest"
|
self.code = code or "BadRequest"
|
||||||
self.title = "Malformed or Missing Payload in Request"
|
self.title = "Malformed or Missing Payload in Request"
|
||||||
self.detail = detail
|
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,
|
def _error(error_code, http_status, error_title, error_detail,
|
||||||
request_id=FAKE_REQUEST_ID):
|
request_id=FAKE_REQUEST_ID):
|
||||||
# responseobj - the response object of Requests framework
|
# 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