Merge "Ensure JSON responses result from failure"

This commit is contained in:
Jenkins 2017-03-28 04:51:31 +00:00 committed by Gerrit Code Review
commit 0e4280ede3
26 changed files with 328 additions and 856 deletions

View File

@ -1,13 +1,13 @@
from datetime import date
import os
from paste import deploy
from flask import Flask, json
from flask import Flask
from oslo_config import cfg
from oslo_log import log as logging
from craton.api import v1
from craton.util import JSON_KWARGS
LOG = logging.getLogger(__name__)
@ -49,27 +49,11 @@ def create_app(global_config, **local_config):
return setup_app()
class JSONEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, date):
return o.isoformat()
return json.JSONEncoder.default(self, o)
RESTFUL_JSON = {
"indent": 2,
"sort_keys": True,
"cls": JSONEncoder,
"separators": (",", ": "),
}
def setup_app(config=None):
app = Flask(__name__, static_folder=None)
app.config.update(
PROPAGATE_EXCEPTIONS=True,
RESTFUL_JSON=RESTFUL_JSON,
RESTFUL_JSON=JSON_KWARGS,
)
app.register_blueprint(v1.bp, url_prefix='/v1')
return app

View File

@ -4,11 +4,9 @@ from oslo_context import context
from oslo_log import log
from oslo_utils import uuidutils
import flask
import json
from craton.db import api as dbapi
from craton import exceptions
from craton.util import handle_all_exceptions_decorator
LOG = log.getLogger(__name__)
@ -34,20 +32,13 @@ class ContextMiddleware(base.Middleware):
request.environ['context'] = ctxt
return ctxt
def _invalid_project_id(self, project_id):
err_msg = json.dumps({
"message": "Project ID ('{}') is not a valid UUID".format(
project_id)
})
return flask.Response(response=err_msg, status=401,
headers={'Content-Type': 'application/json'})
class NoAuthContextMiddleware(ContextMiddleware):
def __init__(self, application):
self.application = application
@handle_all_exceptions_decorator
def process_request(self, request):
# Simply insert some dummy context info
self.make_context(
@ -72,11 +63,16 @@ class LocalAuthContextMiddleware(ContextMiddleware):
def __init__(self, application):
self.application = application
@handle_all_exceptions_decorator
def process_request(self, request):
headers = request.headers
project_id = headers.get('X-Auth-Project')
if not uuidutils.is_uuid_like(project_id):
return self._invalid_project_id(project_id)
raise exceptions.AuthenticationError(
message="Project ID ('{}') is not a valid UUID".format(
project_id
)
)
ctx = self.make_context(
request,
@ -91,7 +87,7 @@ class LocalAuthContextMiddleware(ContextMiddleware):
user_info = dbapi.get_user_info(ctx,
headers.get('X-Auth-User', None))
if user_info.api_key != headers.get('X-Auth-Token', None):
return flask.Response(status=401)
raise exceptions.AuthenticationError
if user_info.is_root:
ctx.is_admin = True
ctx.is_admin_project = True
@ -102,10 +98,7 @@ class LocalAuthContextMiddleware(ContextMiddleware):
ctx.is_admin = False
ctx.is_admin_project = False
except exceptions.NotFound:
return flask.Response(status=401)
except Exception as err:
LOG.error(err)
return flask.Response(status=500)
raise exceptions.AuthenticationError
@classmethod
def factory(cls, global_config, **local_config):
@ -116,11 +109,12 @@ class LocalAuthContextMiddleware(ContextMiddleware):
class KeystoneContextMiddleware(ContextMiddleware):
@handle_all_exceptions_decorator
def process_request(self, request):
headers = request.headers
environ = request.environ
if headers.get('X-Identity-Status', '').lower() != 'confirmed':
return flask.Response(status=401)
raise exceptions.AuthenticationError
token_info = environ['keystone.token_info']['token']
roles = (role['name'] for role in token_info['roles'])

View File

@ -2,10 +2,20 @@ from flask import Blueprint
import flask_restful as restful
from craton.api.v1.routes import routes
from craton.util import handle_all_exceptions
class CratonApi(restful.Api):
def error_router(self, _, e):
return self.handle_error(e)
def handle_error(self, e):
return handle_all_exceptions(e)
bp = Blueprint('v1', __name__)
api = restful.Api(bp, catch_all_404s=True)
api = CratonApi(bp, catch_all_404s=False)
for route in routes:
api.add_resource(route.pop('resource'), *route.pop('urls'), **route)

View File

@ -1,19 +1,13 @@
import functools
import inspect
import json
import re
import urllib.parse as urllib
import decorator
import flask
import flask_restful as restful
from craton import api
from craton.api.v1.validators import ensure_project_exists
from craton.api.v1.validators import request_validate
from craton.api.v1.validators import response_filter
from craton import exceptions
SORT_KEY_SPLITTER = re.compile('[ ,]')
@ -23,30 +17,6 @@ class Resource(restful.Resource):
method_decorators = [request_validate, ensure_project_exists,
response_filter]
def error_response(self, status_code, message):
body = json.dumps(
{
'status': status_code,
'message': message
},
**api.RESTFUL_JSON,
)
resp = flask.make_response("{body}\n".format(body=body))
resp.status_code = status_code
return resp
@decorator.decorator
def http_codes(f, *args, **kwargs):
try:
return f(*args, **kwargs)
except exceptions.Base as err:
return args[0].error_response(err.code, err.message)
except Exception as err:
inspect.getmodule(f).LOG.error(
'Error during %s: %s' % (f.__qualname__, err))
return args[0].error_response(500, 'Unknown Error')
def pagination_context(function):
@functools.wraps(function)

View File

@ -13,7 +13,6 @@ LOG = log.getLogger(__name__)
class Cells(base.Resource):
@base.http_codes
@base.pagination_context
def get(self, context, request_args, pagination_params):
"""Get all cells, with optional filtering."""
@ -30,7 +29,6 @@ class Cells(base.Resource):
response_body = {'cells': cells_obj, 'links': links}
return jsonutils.to_primitive(response_body), 200, None
@base.http_codes
def post(self, context, request_data):
"""Create a new cell."""
json = util.copy_project_id_into_json(context, request_data)
@ -51,19 +49,16 @@ class Cells(base.Resource):
class CellById(base.Resource):
@base.http_codes
def get(self, context, id, request_args):
cell_obj = dbapi.cells_get_by_id(context, id)
cell = utils.get_resource_with_vars(request_args, cell_obj)
return cell, 200, None
@base.http_codes
def put(self, context, id, request_data):
"""Update existing cell."""
cell_obj = dbapi.cells_update(context, id, request_data)
return jsonutils.to_primitive(cell_obj), 200, None
@base.http_codes
def delete(self, context, id):
"""Delete existing cell."""
dbapi.cells_delete(context, id)

View File

@ -13,7 +13,6 @@ LOG = log.getLogger(__name__)
class Clouds(base.Resource):
@base.http_codes
@base.pagination_context
def get(self, context, request_args, pagination_params):
"""Get cloud(s) for the project. Get cloud details if
@ -46,7 +45,6 @@ class Clouds(base.Resource):
response_body = {'clouds': clouds_obj, 'links': links}
return jsonutils.to_primitive(response_body), 200, None
@base.http_codes
def post(self, context, request_data):
"""Create a new cloud."""
json = util.copy_project_id_into_json(context, request_data)
@ -67,20 +65,17 @@ class Clouds(base.Resource):
class CloudsById(base.Resource):
@base.http_codes
def get(self, context, id):
cloud_obj = dbapi.clouds_get_by_id(context, id)
cloud = jsonutils.to_primitive(cloud_obj)
cloud['variables'] = jsonutils.to_primitive(cloud_obj.variables)
return cloud, 200, None
@base.http_codes
def put(self, context, id, request_data):
"""Update existing cloud."""
cloud_obj = dbapi.clouds_update(context, id, request_data)
return jsonutils.to_primitive(cloud_obj), 200, None
@base.http_codes
def delete(self, context, id):
"""Delete existing cloud."""
dbapi.clouds_delete(context, id)

View File

@ -13,7 +13,6 @@ LOG = log.getLogger(__name__)
class Devices(base.Resource):
@base.http_codes
@base.pagination_context
def get(self, context, request_args, pagination_params):
"""Get all devices, with optional filtering."""

View File

@ -13,7 +13,6 @@ LOG = log.getLogger(__name__)
class Hosts(base.Resource):
@base.http_codes
@base.pagination_context
def get(self, context, request_args, pagination_params):
"""Get all hosts for region, with optional filtering."""
@ -35,7 +34,6 @@ class Hosts(base.Resource):
return response_body, 200, None
@base.http_codes
def post(self, context, request_data):
"""Create a new host."""
json = util.copy_project_id_into_json(context, request_data)
@ -58,7 +56,6 @@ class Hosts(base.Resource):
class HostById(base.Resource):
@base.http_codes
def get(self, context, id, request_args):
"""Get host by given id"""
host_obj = dbapi.hosts_get_by_id(context, id)
@ -68,7 +65,6 @@ class HostById(base.Resource):
return host, 200, None
@base.http_codes
def put(self, context, id, request_data):
"""Update existing host data, or create if it does not exist."""
host_obj = dbapi.hosts_update(context, id, request_data)
@ -79,7 +75,6 @@ class HostById(base.Resource):
return host, 200, None
@base.http_codes
def delete(self, context, id):
"""Delete existing host."""
dbapi.hosts_delete(context, id)
@ -88,14 +83,12 @@ class HostById(base.Resource):
class HostsLabels(base.Resource):
@base.http_codes
def get(self, context, id):
"""Get labels for given host device."""
host_obj = dbapi.hosts_get_by_id(context, id)
response = {"labels": list(host_obj.labels)}
return response, 200, None
@base.http_codes
def put(self, context, id, request_data):
"""
Update existing device label entirely, or add if it does
@ -105,7 +98,6 @@ class HostsLabels(base.Resource):
response = {"labels": list(resp.labels)}
return response, 200, None
@base.http_codes
def delete(self, context, id, request_data):
"""Delete device label entirely."""
dbapi.hosts_labels_delete(context, id, request_data)

View File

@ -14,7 +14,6 @@ LOG = log.getLogger(__name__)
class Networks(base.Resource):
"""Controller for Networks resources."""
@base.http_codes
@base.pagination_context
def get(self, context, request_args, pagination_params):
"""Get all networks, with optional filtering."""
@ -30,7 +29,6 @@ class Networks(base.Resource):
response_body = {'networks': networks_obj, 'links': links}
return jsonutils.to_primitive(response_body), 200, None
@base.http_codes
def post(self, context, request_data):
"""Create a new network."""
json = util.copy_project_id_into_json(context, request_data)
@ -53,7 +51,6 @@ class Networks(base.Resource):
class NetworkById(base.Resource):
"""Controller for Networks by ID."""
@base.http_codes
def get(self, context, id):
"""Get network by given id"""
obj = dbapi.networks_get_by_id(context, id)
@ -61,13 +58,11 @@ class NetworkById(base.Resource):
device['variables'] = jsonutils.to_primitive(obj.variables)
return device, 200, None
@base.http_codes
def put(self, context, id, request_data):
"""Update existing network values."""
net_obj = dbapi.networks_update(context, id, request_data)
return jsonutils.to_primitive(net_obj), 200, None
@base.http_codes
def delete(self, context, id):
"""Delete existing network."""
dbapi.networks_delete(context, id)
@ -77,7 +72,6 @@ class NetworkById(base.Resource):
class NetworkDevices(base.Resource):
"""Controller for Network Device resources."""
@base.http_codes
@base.pagination_context
def get(self, context, request_args, pagination_params):
"""Get all network devices."""
@ -99,7 +93,6 @@ class NetworkDevices(base.Resource):
return response_body, 200, None
@base.http_codes
def post(self, context, request_data):
"""Create a new network device."""
json = util.copy_project_id_into_json(context, request_data)
@ -123,7 +116,6 @@ class NetworkDevices(base.Resource):
class NetworkDeviceById(base.Resource):
"""Controller for Network Devices by ID."""
@base.http_codes
def get(self, context, id, request_args):
"""Get network device by given id"""
obj = dbapi.network_devices_get_by_id(context, id)
@ -135,7 +127,6 @@ class NetworkDeviceById(base.Resource):
return device, 200, None
@base.http_codes
def put(self, context, id, request_data):
"""Update existing device values."""
net_obj = dbapi.network_devices_update(context, id, request_data)
@ -145,7 +136,6 @@ class NetworkDeviceById(base.Resource):
return device, 200, None
@base.http_codes
def delete(self, context, id):
"""Delete existing network device."""
dbapi.network_devices_delete(context, id)
@ -155,21 +145,18 @@ class NetworkDeviceById(base.Resource):
class NetworkDeviceLabels(base.Resource):
"""Controller for Netowrk Device Labels."""
@base.http_codes
def get(self, context, id):
"""Get labels for given network device."""
obj = dbapi.network_devices_get_by_id(context, id)
response = {"labels": list(obj.labels)}
return response, 200, None
@base.http_codes
def put(self, context, id, request_data):
"""Update existing device label. Adds if it does not exist."""
resp = dbapi.network_devices_labels_update(context, id, request_data)
response = {"labels": list(resp.labels)}
return response, 200, None
@base.http_codes
def delete(self, context, id, request_data):
"""Delete device label(s)."""
dbapi.network_devices_labels_delete(context, id, request_data)
@ -179,7 +166,6 @@ class NetworkDeviceLabels(base.Resource):
class NetworkInterfaces(base.Resource):
"""Controller for Netowrk Interfaces."""
@base.http_codes
@base.pagination_context
def get(self, context, request_args, pagination_params):
"""Get all network interfaces."""
@ -190,7 +176,6 @@ class NetworkInterfaces(base.Resource):
response_body = {'network_interfaces': interfaces_obj, 'links': links}
return jsonutils.to_primitive(response_body), 200, None
@base.http_codes
def post(self, context, request_data):
"""Create a new network interface."""
json = util.copy_project_id_into_json(context, request_data)
@ -207,7 +192,6 @@ class NetworkInterfaces(base.Resource):
class NetworkInterfaceById(base.Resource):
@base.http_codes
def get(self, context, id):
"""Get network interface by given id"""
obj = dbapi.network_interfaces_get_by_id(context, id)
@ -215,13 +199,11 @@ class NetworkInterfaceById(base.Resource):
interface['variables'] = jsonutils.to_primitive(obj.variables)
return interface, 200, None
@base.http_codes
def put(self, context, id, request_data):
"""Update existing network interface values."""
net_obj = dbapi.network_interfaces_update(context, id, request_data)
return jsonutils.to_primitive(net_obj), 200, None
@base.http_codes
def delete(self, context, id):
"""Delete existing network interface."""
dbapi.network_interfaces_delete(context, id)

View File

@ -13,7 +13,6 @@ LOG = log.getLogger(__name__)
class Regions(base.Resource):
@base.http_codes
@base.pagination_context
def get(self, context, request_args, pagination_params):
"""Get region(s) for the project. Get region details if
@ -46,7 +45,6 @@ class Regions(base.Resource):
response_body = {'regions': regions_obj, 'links': links}
return jsonutils.to_primitive(response_body), 200, None
@base.http_codes
def post(self, context, request_data):
"""Create a new region."""
json = util.copy_project_id_into_json(context, request_data)
@ -67,19 +65,16 @@ class Regions(base.Resource):
class RegionsById(base.Resource):
@base.http_codes
def get(self, context, id, request_args):
region_obj = dbapi.regions_get_by_id(context, id)
region = utils.get_resource_with_vars(request_args, region_obj)
return region, 200, None
@base.http_codes
def put(self, context, id, request_data):
"""Update existing region."""
region_obj = dbapi.regions_update(context, id, request_data)
return jsonutils.to_primitive(region_obj), 200, None
@base.http_codes
def delete(self, context, id):
"""Delete existing region."""
dbapi.regions_delete(context, id)

View File

@ -12,7 +12,6 @@ LOG = log.getLogger(__name__)
class Projects(base.Resource):
@base.http_codes
@base.pagination_context
def get(self, context, request_args, pagination_params):
"""Get all projects. Requires super admin privileges."""
@ -35,7 +34,6 @@ class Projects(base.Resource):
response_body = {'projects': projects_obj, 'links': links}
return jsonutils.to_primitive(response_body), 200, None
@base.http_codes
def post(self, context, request_data):
"""Create a new project. Requires super admin privileges."""
project_obj = dbapi.projects_create(context, request_data)
@ -56,7 +54,6 @@ class Projects(base.Resource):
class ProjectById(base.Resource):
@base.http_codes
def get(self, context, id):
"""Get a project details by id. Requires super admin privileges."""
project_obj = dbapi.projects_get_by_id(context, id)
@ -64,7 +61,6 @@ class ProjectById(base.Resource):
project['variables'] = jsonutils.to_primitive(project_obj.variables)
return project, 200, None
@base.http_codes
def delete(self, context, id):
"""Delete existing project. Requires super admin privileges."""
dbapi.projects_delete(context, id)

View File

@ -12,7 +12,6 @@ LOG = log.getLogger(__name__)
class Users(base.Resource):
@base.http_codes
@base.pagination_context
def get(self, context, request_args, pagination_params):
"""Get all users. Requires project admin privileges."""
@ -37,7 +36,6 @@ class Users(base.Resource):
response_body = {'users': users_obj, 'links': links}
return jsonutils.to_primitive(response_body), 200, None
@base.http_codes
def post(self, context, request_data):
"""Create a new user. Requires project admin privileges."""
# NOTE(sulo): Instead of using context project_id from
@ -59,13 +57,11 @@ class Users(base.Resource):
class UserById(base.Resource):
@base.http_codes
def get(self, context, id):
"""Get a user details by id. Requires project admin privileges."""
user_obj = dbapi.users_get_by_id(context, id)
return jsonutils.to_primitive(user_obj), 200, None
@base.http_codes
def delete(self, context, id):
"""Delete existing user. Requires project admin privileges."""
dbapi.users_delete(context, id)

View File

@ -14,7 +14,6 @@ LOG = log.getLogger(__name__)
class Variables(base.Resource):
@base.http_codes
def get(self, context, resources, id, request_args=None):
"""Get variables for given resource."""
obj = dbapi.resource_get_by_id(context, resources, id)
@ -22,7 +21,6 @@ class Variables(base.Resource):
resp = {"variables": jsonutils.to_primitive(obj.vars)}
return resp, 200, None
@base.http_codes
def put(self, context, resources, id, request_data):
"""
Update existing resource variables, or create if it does
@ -34,7 +32,6 @@ class Variables(base.Resource):
resp = {"variables": jsonutils.to_primitive(obj.variables)}
return resp, 200, None
@base.http_codes
def delete(self, context, resources, id, request_data):
"""Delete resource variables."""
# NOTE(sulo): this is not that great. Find a better way to do this.

File diff suppressed because it is too large Load Diff

View File

@ -4,9 +4,7 @@
from functools import wraps
from werkzeug.datastructures import MultiDict, Headers
from flask import request, current_app
from flask_restful import abort
from flask_restful.utils import unpack
from flask import request
from jsonschema import Draft4Validator
from oslo_log import log
@ -182,9 +180,12 @@ class FlaskValidatorAdaptor(object):
def validate(self, value):
value = self.type_convert(value)
errors = list(e.message for e in self.validator.iter_errors(value))
errors = sorted(e.message for e in self.validator.iter_errors(value))
if errors:
abort(400, message='Bad Request', errors=errors)
msg = "The request included the following errors:\n- {}".format(
"\n- ".join(errors)
)
raise exceptions.BadRequest(message=msg)
return merge_default(self.validator.schema, value)
@ -233,9 +234,6 @@ def response_filter(view):
def wrapper(*args, **kwargs):
resp = view(*args, **kwargs)
if isinstance(resp, current_app.response_class):
return resp
endpoint = request.endpoint.partition('.')[-1]
method = request.method
if method == 'HEAD':
@ -248,12 +246,9 @@ def response_filter(view):
'filters.',
{"endpoint": endpoint, "method": method}
)
abort(500)
raise exceptions.UnknownException
headers = None
status = None
if isinstance(resp, tuple):
resp, status, headers = unpack(resp)
body, status, headers = resp
try:
schemas = resp_filter[status]
@ -263,17 +258,18 @@ def response_filter(view):
'filter "(%(endpoint)s, %(method)s)".',
{"status": status, "endpoint": endpoint, "method": method}
)
abort(500)
raise exceptions.UnknownException
resp, errors = normalize(schemas['schema'], resp)
body, errors = normalize(schemas['schema'], body)
if schemas['headers']:
headers, header_errors = normalize(
{'properties': schemas['headers']}, headers)
errors.extend(header_errors)
if errors:
abort(500, message='Expectation Failed', errors=errors)
LOG.error('Expectation Failed: %s', errors)
raise exceptions.UnknownException
return resp, status, headers
return body, status, headers
return wrapper

View File

@ -65,6 +65,11 @@ class DeviceNotFound(Base):
msg = "%(device_type)s device not found for ID %(id)s."
class AuthenticationError(Base):
code = 401
msg = "The request could not be authenticated."
class AdminRequired(Base):
code = 401
msg = "This action requires the 'admin' role"

View File

@ -240,11 +240,19 @@ class TestCase(testtools.TestCase):
response.text
)
def assertFailureFormat(self, response):
if response.status_code >= 400:
body = response.json()
self.assertEqual(2, len(body))
self.assertEqual(response.status_code, body["status"])
self.assertIn("message", body)
def get(self, url, headers=None, **params):
resp = self.session.get(
url, verify=False, headers=headers, params=params,
)
self.assertJSON(resp)
self.assertFailureFormat(resp)
return resp
def post(self, url, headers=None, data=None):
@ -252,6 +260,7 @@ class TestCase(testtools.TestCase):
url, verify=False, headers=headers, json=data,
)
self.assertJSON(resp)
self.assertFailureFormat(resp)
return resp
def put(self, url, headers=None, data=None):
@ -259,6 +268,7 @@ class TestCase(testtools.TestCase):
url, verify=False, headers=headers, json=data,
)
self.assertJSON(resp)
self.assertFailureFormat(resp)
return resp
def delete(self, url, headers=None, body=None):
@ -266,6 +276,7 @@ class TestCase(testtools.TestCase):
url, verify=False, headers=headers, json=body,
)
self.assertJSON(resp)
self.assertFailureFormat(resp)
return resp
def create_project(self, name, headers=None, variables=None):

View File

@ -64,8 +64,11 @@ class APIV1CellTest(APIV1ResourceWithVariablesTestCase):
'cloud_id': self.cloud['id'], 'name': 'a', 'id': 3}
cell = self.post(url, data=payload)
self.assertEqual(400, cell.status_code)
msg = ["Additional properties are not allowed ('id' was unexpected)"]
self.assertEqual(cell.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed ('id' was unexpected)"
)
self.assertEqual(cell.json()['message'], msg)
def test_cell_create_with_extra_created_at_property_fails(self):
url = self.url + '/v1/cells'
@ -74,9 +77,12 @@ class APIV1CellTest(APIV1ResourceWithVariablesTestCase):
'created_at': "some date"}
cell = self.post(url, data=payload)
self.assertEqual(400, cell.status_code)
msg = ["Additional properties are not allowed "
"('created_at' was unexpected)"]
self.assertEqual(cell.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed "
"('created_at' was unexpected)"
)
self.assertEqual(cell.json()['message'], msg)
def test_cell_create_with_extra_updated_at_property_fails(self):
url = self.url + '/v1/cells'
@ -85,9 +91,24 @@ class APIV1CellTest(APIV1ResourceWithVariablesTestCase):
'updated_at': "some date"}
cell = self.post(url, data=payload)
self.assertEqual(400, cell.status_code)
msg = ["Additional properties are not allowed "
"('updated_at' was unexpected)"]
self.assertEqual(cell.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed "
"('updated_at' was unexpected)"
)
self.assertEqual(cell.json()['message'], msg)
def test_cell_create_missing_all_properties_fails(self):
url = self.url + '/v1/cells'
cell = self.post(url, data={})
self.assertEqual(400, cell.status_code)
msg = (
"The request included the following errors:\n"
"- 'cloud_id' is a required property\n"
"- 'name' is a required property\n"
"- 'region_id' is a required property"
)
self.assertEqual(cell.json()['message'], msg)
def test_cells_get_all_with_details(self):
self.create_cell('cell1', variables={'a': 'b'})

View File

@ -40,8 +40,11 @@ class APIV1CloudTest(TestCase):
url = self.url + '/v1/clouds'
resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400)
err_msg = ["'name' is a required property"]
self.assertEqual(resp.json()['errors'], err_msg)
err_msg = (
"The request included the following errors:\n"
"- 'name' is a required property"
)
self.assertEqual(resp.json()['message'], err_msg)
def test_create_cloud_with_duplicate_name_fails(self):
self.create_cloud("ORD135")
@ -56,26 +59,45 @@ class APIV1CloudTest(TestCase):
url = self.url + '/v1/clouds'
resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400)
msg = ["Additional properties are not allowed ('id' was unexpected)"]
self.assertEqual(resp.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed ('id' was unexpected)"
)
self.assertEqual(resp.json()['message'], msg)
def test_create_region_with_extra_created_at_property_fails(self):
values = {"name": "test", "created_at": "some date"}
url = self.url + '/v1/clouds'
resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400)
msg = ["Additional properties are not allowed "
"('created_at' was unexpected)"]
self.assertEqual(resp.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed "
"('created_at' was unexpected)"
)
self.assertEqual(resp.json()['message'], msg)
def test_create_region_with_extra_updated_at_property_fails(self):
values = {"name": "test", "updated_at": "some date"}
url = self.url + '/v1/clouds'
resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400)
msg = ["Additional properties are not allowed "
"('updated_at' was unexpected)"]
self.assertEqual(resp.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed "
"('updated_at' was unexpected)"
)
self.assertEqual(resp.json()['message'], msg)
def test_cloud_create_missing_all_properties_fails(self):
url = self.url + '/v1/clouds'
cloud = self.post(url, data={})
self.assertEqual(400, cloud.status_code)
msg = (
"The request included the following errors:\n"
"- 'name' is a required property"
)
self.assertEqual(cloud.json()['message'], msg)
def test_clouds_get_all(self):
self.create_cloud("ORD1")

View File

@ -69,8 +69,11 @@ class APIV1HostTest(DeviceTestBase, APIV1ResourceWithVariablesTestCase):
'cloud_id': self.cloud['id'], 'name': 'a', 'id': 1}
host = self.post(url, data=payload)
self.assertEqual(400, host.status_code)
msg = ["Additional properties are not allowed ('id' was unexpected)"]
self.assertEqual(host.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed ('id' was unexpected)"
)
self.assertEqual(host.json()['message'], msg)
def test_create_with_extra_created_at_property_fails(self):
url = self.url + '/v1/hosts'
@ -80,9 +83,12 @@ class APIV1HostTest(DeviceTestBase, APIV1ResourceWithVariablesTestCase):
'created_at': 'some date'}
host = self.post(url, data=payload)
self.assertEqual(400, host.status_code)
msg = ["Additional properties are not allowed "
"('created_at' was unexpected)"]
self.assertEqual(host.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed "
"('created_at' was unexpected)"
)
self.assertEqual(host.json()['message'], msg)
def test_create_with_extra_updated_at_property_fails(self):
url = self.url + '/v1/hosts'
@ -92,9 +98,26 @@ class APIV1HostTest(DeviceTestBase, APIV1ResourceWithVariablesTestCase):
'updated_at': 'some date'}
host = self.post(url, data=payload)
self.assertEqual(400, host.status_code)
msg = ["Additional properties are not allowed "
"('updated_at' was unexpected)"]
self.assertEqual(host.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed "
"('updated_at' was unexpected)"
)
self.assertEqual(host.json()['message'], msg)
def test_create_missing_all_properties_fails(self):
url = self.url + '/v1/hosts'
host = self.post(url, data={})
self.assertEqual(400, host.status_code)
msg = (
"The request included the following errors:\n"
"- 'cloud_id' is a required property\n"
"- 'device_type' is a required property\n"
"- 'ip_address' is a required property\n"
"- 'name' is a required property\n"
"- 'region_id' is a required property"
)
self.assertEqual(host.json()['message'], msg)
def test_create_with_parent_id(self):
parent = self.create_host(

View File

@ -42,8 +42,11 @@ class APIV1NetworkSchemaTest(TestCase):
}
network = self.post(self.networks_url, data=payload)
self.assertEqual(400, network.status_code)
msg = ["'region_id' is a required property"]
self.assertEqual(network.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- 'region_id' is a required property"
)
self.assertEqual(network.json()['message'], msg)
def test_network_create_without_cloud_id_fails(self):
payload = {
@ -55,8 +58,11 @@ class APIV1NetworkSchemaTest(TestCase):
}
network = self.post(self.networks_url, data=payload)
self.assertEqual(400, network.status_code)
msg = ["'cloud_id' is a required property"]
self.assertEqual(network.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- 'cloud_id' is a required property"
)
self.assertEqual(network.json()['message'], msg)
def test_network_create_with_extra_id_property_fails(self):
payload = {
@ -70,8 +76,11 @@ class APIV1NetworkSchemaTest(TestCase):
}
network = self.post(self.networks_url, data=payload)
self.assertEqual(400, network.status_code)
msg = ["Additional properties are not allowed ('id' was unexpected)"]
self.assertEqual(network.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed ('id' was unexpected)"
)
self.assertEqual(network.json()['message'], msg)
def test_network_create_with_extra_created_at_property_fails(self):
payload = {
@ -85,9 +94,12 @@ class APIV1NetworkSchemaTest(TestCase):
}
network = self.post(self.networks_url, data=payload)
self.assertEqual(400, network.status_code)
msg = ["Additional properties are not allowed ('created_at' was "
"unexpected)"]
self.assertEqual(network.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed ('created_at' was "
"unexpected)"
)
self.assertEqual(network.json()['message'], msg)
def test_network_create_with_extra_updated_at_property_fails(self):
payload = {
@ -101,9 +113,27 @@ class APIV1NetworkSchemaTest(TestCase):
}
network = self.post(self.networks_url, data=payload)
self.assertEqual(400, network.status_code)
msg = ["Additional properties are not allowed ('updated_at' was "
"unexpected)"]
self.assertEqual(network.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed ('updated_at' was "
"unexpected)"
)
self.assertEqual(network.json()['message'], msg)
def test_network_create_missing_all_properties_fails(self):
url = self.url + '/v1/networks'
network = self.post(url, data={})
self.assertEqual(400, network.status_code)
msg = (
"The request included the following errors:\n"
"- 'cidr' is a required property\n"
"- 'cloud_id' is a required property\n"
"- 'gateway' is a required property\n"
"- 'name' is a required property\n"
"- 'netmask' is a required property\n"
"- 'region_id' is a required property"
)
self.assertEqual(network.json()['message'], msg)
def test_network_get_all_with_details(self):
payload = {

View File

@ -99,3 +99,17 @@ class APIV1NetworkDeviceTest(DeviceTestBase):
url, data={'parent_id': grandchild['id']}
)
self.assertEqual(400, parent_update_resp.status_code)
def test_network_device_create_missing_all_properties_fails(self):
url = self.url + '/v1/network-devices'
network_device = self.post(url, data={})
self.assertEqual(400, network_device.status_code)
msg = (
"The request included the following errors:\n"
"- 'cloud_id' is a required property\n"
"- 'device_type' is a required property\n"
"- 'ip_address' is a required property\n"
"- 'name' is a required property\n"
"- 'region_id' is a required property"
)
self.assertEqual(network_device.json()['message'], msg)

View File

@ -55,3 +55,16 @@ class APIv1NetworkInterfacesTest(functional.DeviceTestBase):
payload = {'port': 'asdf'}
response = self.put(url, data=payload)
self.assertBadRequest(response)
def test_network_interface_create_missing_all_properties_fails(self):
url = self.url + '/v1/network-interfaces'
network_interface = self.post(url, data={})
self.assertEqual(400, network_interface.status_code)
msg = (
"The request included the following errors:\n"
"- 'device_id' is a required property\n"
"- 'interface_type' is a required property\n"
"- 'ip_address' is a required property\n"
"- 'name' is a required property"
)
self.assertEqual(network_interface.json()['message'], msg)

View File

@ -139,3 +139,13 @@ class APIV1ProjectTest(ProjectTests, APIV1ResourceWithVariablesTestCase):
variables=variables)
self.assert_vars_get_expected(project['id'], variables)
self.assert_vars_can_be_deleted(project['id'])
def test_project_create_missing_all_properties_fails(self):
url = self.url + '/v1/projects'
project = self.post(url, data={})
self.assertEqual(400, project.status_code)
msg = (
"The request included the following errors:\n"
"- 'name' is a required property"
)
self.assertEqual(project.json()['message'], msg)

View File

@ -63,16 +63,22 @@ class APIV1RegionTest(RegionTests):
url = self.url + '/v1/regions'
resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400)
err_msg = ["'name' is a required property"]
self.assertEqual(resp.json()['errors'], err_msg)
err_msg = (
"The request included the following errors:\n"
"- 'name' is a required property"
)
self.assertEqual(resp.json()['message'], err_msg)
def test_create_region_with_no_cloud_id_fails(self):
values = {"name": "I don't work at all, you know."}
url = self.url + '/v1/regions'
resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400)
err_msg = ["'cloud_id' is a required property"]
self.assertEqual(resp.json()['errors'], err_msg)
err_msg = (
"The request included the following errors:\n"
"- 'cloud_id' is a required property"
)
self.assertEqual(resp.json()['message'], err_msg)
def test_create_region_with_duplicate_name_fails(self):
self.create_region("ORD135")
@ -87,8 +93,11 @@ class APIV1RegionTest(RegionTests):
url = self.url + '/v1/regions'
resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400)
msg = ["Additional properties are not allowed ('id' was unexpected)"]
self.assertEqual(resp.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed ('id' was unexpected)"
)
self.assertEqual(resp.json()['message'], msg)
def test_create_region_with_extra_created_at_property_fails(self):
values = {"name": "test", 'cloud_id': self.cloud['id'],
@ -96,9 +105,12 @@ class APIV1RegionTest(RegionTests):
url = self.url + '/v1/regions'
resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400)
msg = ["Additional properties are not allowed "
"('created_at' was unexpected)"]
self.assertEqual(resp.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed "
"('created_at' was unexpected)"
)
self.assertEqual(resp.json()['message'], msg)
def test_create_region_with_extra_updated_at_property_fails(self):
values = {"name": "test", 'cloud_id': self.cloud['id'],
@ -106,9 +118,23 @@ class APIV1RegionTest(RegionTests):
url = self.url + '/v1/regions'
resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400)
msg = ["Additional properties are not allowed "
"('updated_at' was unexpected)"]
self.assertEqual(resp.json()['errors'], msg)
msg = (
"The request included the following errors:\n"
"- Additional properties are not allowed "
"('updated_at' was unexpected)"
)
self.assertEqual(resp.json()['message'], msg)
def test_region_create_missing_all_properties_fails(self):
url = self.url + '/v1/regions'
region = self.post(url, data={})
self.assertEqual(400, region.status_code)
msg = (
"The request included the following errors:\n"
"- 'cloud_id' is a required property\n"
"- 'name' is a required property"
)
self.assertEqual(region.json()['message'], msg)
def test_regions_get_all(self):
self.create_region("ORD1")

View File

@ -1,4 +1,14 @@
"""Module containing generic utilies for Craton."""
from datetime import date
from decorator import decorator
from flask import json, Response
import werkzeug.exceptions
from oslo_log import log
import craton.exceptions as exceptions
LOG = log.getLogger(__name__)
def copy_project_id_into_json(context, json, project_id_key='project_id'):
@ -16,3 +26,57 @@ def copy_project_id_into_json(context, json, project_id_key='project_id'):
"""
json[project_id_key] = getattr(context, 'tenant', '')
return json
class JSONEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, date):
return o.isoformat()
return json.JSONEncoder.default(self, o)
JSON_KWARGS = {
"indent": 2,
"sort_keys": True,
"cls": JSONEncoder,
"separators": (",", ": "),
}
def handle_all_exceptions(e):
"""Generate error Flask response object from exception."""
headers = [("Content-Type", "application/json")]
if isinstance(e, exceptions.Base):
message = e.message
status = e.code
elif isinstance(e, werkzeug.exceptions.HTTPException):
message = e.description
status = e.code
# Werkzeug exceptions can include additional headers, those should be
# kept unless the header is "Content-Type" which is set by this
# function.
headers.extend(
h for h in e.get_headers(None) if h[0].lower() != "content-type"
)
else:
LOG.exception(e)
e_ = exceptions.UnknownException
message = e_.message
status = e_.code
body = {
"message": message,
"status": status,
}
body_ = "{}\n".format(json.dumps(body, **JSON_KWARGS))
return Response(body_, status, headers)
@decorator
def handle_all_exceptions_decorator(fn, *args, **kwargs):
try:
return fn(*args, **kwargs)
except Exception as e:
return handle_all_exceptions(e)