Add validation for incomming test results
Test results are validated using jsonschema. Actual schema can be retrieved from /v1/results/schema endpoint. https://github.com/stackforge/refstack/blob/master/specs/proposed/refstack-org-test-result-json-schema.rst Change-Id: I05e5fa3b84c60925f162985cd7d51b14d69b19d8
This commit is contained in:
parent
4e390ee67b
commit
7e8af456c4
@ -20,47 +20,74 @@ import pecan
|
|||||||
from pecan import rest
|
from pecan import rest
|
||||||
|
|
||||||
from refstack import db
|
from refstack import db
|
||||||
|
from refstack.common import validators
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ResultsController(rest.RestController):
|
class RestControllerWithValidation(rest.RestController):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Controller provides validation for POSTed data
|
||||||
|
exposed endpoints:
|
||||||
|
POST base_url/
|
||||||
|
GET base_url/<item uid>
|
||||||
|
GET base_url/schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, validator):
|
||||||
|
self.validator = validator
|
||||||
|
|
||||||
|
def get_item(self, item_id):
|
||||||
|
"""Handler for getting item"""
|
||||||
|
raise NotImplemented
|
||||||
|
|
||||||
|
def store_item(self, item_in_json):
|
||||||
|
"""Handler for storing item. Should return new item id"""
|
||||||
|
raise NotImplemented
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
def get_one(self, arg):
|
||||||
|
"""Return test results in JSON format.
|
||||||
|
:param arg: item ID in uuid4 format or action
|
||||||
|
"""
|
||||||
|
if self.validator.assert_id(arg):
|
||||||
|
return self.get_item(item_id=arg)
|
||||||
|
|
||||||
|
elif arg == 'schema':
|
||||||
|
return self.validator.schema
|
||||||
|
|
||||||
|
else:
|
||||||
|
pecan.abort(404)
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
def post(self, ):
|
||||||
|
"""POST handler."""
|
||||||
|
item = validators.safe_load_json_body(self.validator)
|
||||||
|
item_id = self.store_item(item)
|
||||||
|
pecan.response.status = 201
|
||||||
|
return item_id
|
||||||
|
|
||||||
|
|
||||||
|
class ResultsController(RestControllerWithValidation):
|
||||||
|
|
||||||
"""/v1/results handler."""
|
"""/v1/results handler."""
|
||||||
|
|
||||||
@pecan.expose('json')
|
def get_item(self, item_id):
|
||||||
def get(self, ):
|
"""Handler for getting item"""
|
||||||
"""GET handler."""
|
test_info = db.get_test(item_id)
|
||||||
return {'Result': 'Ok'}
|
|
||||||
|
|
||||||
@pecan.expose("json")
|
|
||||||
def get_one(self, test_id):
|
|
||||||
"""Return test results in JSON format.
|
|
||||||
|
|
||||||
:param test_id: ID of the test to get the JSON for.
|
|
||||||
"""
|
|
||||||
test_info = db.get_test(test_id)
|
|
||||||
if not test_info:
|
if not test_info:
|
||||||
pecan.abort(404)
|
pecan.abort(404)
|
||||||
|
test_list = db.get_test_results(item_id)
|
||||||
test_list = db.get_test_results(test_id)
|
|
||||||
test_name_list = [test_dict[0] for test_dict in test_list]
|
test_name_list = [test_dict[0] for test_dict in test_list]
|
||||||
return {"cpid": test_info.cpid,
|
return {"cpid": test_info.cpid,
|
||||||
"created_at": test_info.created_at,
|
"created_at": test_info.created_at,
|
||||||
"duration_seconds": test_info.duration_seconds,
|
"duration_seconds": test_info.duration_seconds,
|
||||||
"results": test_name_list}
|
"results": test_name_list}
|
||||||
|
|
||||||
@pecan.expose(template='json')
|
def store_item(self, item_in_json):
|
||||||
def post(self, ):
|
"""Handler for storing item. Should return new item id"""
|
||||||
"""POST handler."""
|
test_id = db.store_results(item_in_json)
|
||||||
try:
|
|
||||||
results = pecan.request.json
|
|
||||||
except ValueError:
|
|
||||||
return pecan.abort(400,
|
|
||||||
detail='Request body \'%s\' could not '
|
|
||||||
'be decoded as JSON.'
|
|
||||||
'' % pecan.request.body)
|
|
||||||
test_id = db.store_results(results)
|
|
||||||
return {'test_id': test_id}
|
return {'test_id': test_id}
|
||||||
|
|
||||||
|
|
||||||
@ -68,4 +95,4 @@ class V1Controller(object):
|
|||||||
|
|
||||||
"""Version 1 API controller root."""
|
"""Version 1 API controller root."""
|
||||||
|
|
||||||
results = ResultsController()
|
results = ResultsController(validators.TestResultValidator())
|
||||||
|
0
refstack/common/__init__.py
Normal file
0
refstack/common/__init__.py
Normal file
121
refstack/common/validators.py
Normal file
121
refstack/common/validators.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
""" Validators module
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import json
|
||||||
|
import jsonschema
|
||||||
|
import pecan
|
||||||
|
from pecan import request
|
||||||
|
|
||||||
|
ext_format_checker = jsonschema.FormatChecker()
|
||||||
|
|
||||||
|
|
||||||
|
def is_uuid(inst):
|
||||||
|
""" Check that inst is a uuid_hex string. """
|
||||||
|
try:
|
||||||
|
uuid.UUID(hex=inst)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@jsonschema.FormatChecker.checks(ext_format_checker,
|
||||||
|
format='uuid_hex',
|
||||||
|
raises=(TypeError, ValueError))
|
||||||
|
def checker_uuid(inst):
|
||||||
|
"""Checker 'uuid_hex' format for jsonschema validator"""
|
||||||
|
return is_uuid(inst)
|
||||||
|
|
||||||
|
|
||||||
|
class Validator(object):
|
||||||
|
|
||||||
|
"""Base class for validators"""
|
||||||
|
|
||||||
|
def validate(self, json_data):
|
||||||
|
"""
|
||||||
|
:param json_data: data for validation
|
||||||
|
"""
|
||||||
|
jsonschema.validate(json_data, self.schema)
|
||||||
|
|
||||||
|
|
||||||
|
class TestResultValidator(Validator):
|
||||||
|
|
||||||
|
"""Validator for incoming test results."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.schema = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'cpid': {
|
||||||
|
'type': 'string'
|
||||||
|
},
|
||||||
|
'duration_seconds': {'type': 'integer'},
|
||||||
|
'results': {
|
||||||
|
"type": "array",
|
||||||
|
"items": [{
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'name': {'type': 'string'},
|
||||||
|
'uid': {
|
||||||
|
'type': 'string',
|
||||||
|
'format': 'uuid_hex'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'required': ['cpid', 'duration_seconds', 'results'],
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
jsonschema.Draft4Validator.check_schema(self.schema)
|
||||||
|
self.validator = jsonschema.Draft4Validator(
|
||||||
|
self.schema,
|
||||||
|
format_checker=ext_format_checker
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def assert_id(_id):
|
||||||
|
""" Check that _id is a valid uuid_hex string. """
|
||||||
|
return is_uuid(_id)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_load_json_body(validator):
|
||||||
|
"""
|
||||||
|
Helper for load validated request body
|
||||||
|
:param validator: instance of Validator class
|
||||||
|
:return validated body
|
||||||
|
:raise ValueError, jsonschema.ValidationError
|
||||||
|
"""
|
||||||
|
body = ''
|
||||||
|
try:
|
||||||
|
body = json.loads(request.body)
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
pecan.abort(400, detail=e.message)
|
||||||
|
|
||||||
|
try:
|
||||||
|
validator.validate(body)
|
||||||
|
except jsonschema.ValidationError as e:
|
||||||
|
pecan.abort(400,
|
||||||
|
detail=e.message,
|
||||||
|
title='Malformed json data, '
|
||||||
|
'see %s/schema' % request.path_url)
|
||||||
|
|
||||||
|
return body
|
@ -8,3 +8,4 @@ pecan>=0.8.2
|
|||||||
pyOpenSSL==0.13
|
pyOpenSSL==0.13
|
||||||
pycrypto==2.6
|
pycrypto==2.6
|
||||||
requests==1.2.3
|
requests==1.2.3
|
||||||
|
jsonschema>=2.0.0,<3.0.0
|
@ -76,12 +76,12 @@ https://github.com/stackforge/refstack/blob/master/specs/approved/api-v1.md
|
|||||||
**failed response:** http:400 - Malformed data.
|
**failed response:** http:400 - Malformed data.
|
||||||
|
|
||||||
{
|
{
|
||||||
'message': 'malformed json data, see /v1/schema/results.json'
|
'message': 'Malformed json data, see /v1/results/schema'
|
||||||
}
|
}
|
||||||
|
|
||||||
**url:** get /v1/schema/results.json
|
**url:** get /v1/results/schema
|
||||||
|
|
||||||
**valid response:** http:200 results.json file
|
**valid response:** http:200 schema.json file
|
||||||
|
|
||||||
No invalid responses. No accepted parameters.
|
No invalid responses. No accepted parameters.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user