Handle API response errors (#14)

Assert that exception message is not empty
Added voluptuous
This commit is contained in:
Paul Millette 2016-05-03 15:15:49 -04:00 committed by Maxime Belanger
parent c621ede3ca
commit 00f5d6994b
12 changed files with 336 additions and 40 deletions

View File

@ -18,6 +18,7 @@ import jsonpickle
from datetime import datetime from datetime import datetime
from functools import wraps from functools import wraps
from almanach.common.validation_exception import InvalidAttributeException
from flask import Blueprint, Response, request from flask import Blueprint, Response, request
from werkzeug.wrappers import BaseResponse from werkzeug.wrappers import BaseResponse
@ -49,6 +50,9 @@ def to_json(api_call):
message = "The request you have made must have data. None was given." message = "The request you have made must have data. None was given."
logging.warning(message) logging.warning(message)
return encode({"error": message}), 400, {"Content-Type": "application/json"} return encode({"error": message}), 400, {"Content-Type": "application/json"}
except InvalidAttributeException as e:
logging.warning(e.get_error_message())
return encode({"error": e.get_error_message()}), 400, {"Content-Type": "application/json"}
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
return Response(encode({"error": e.message}), 500, {"Content-Type": "application/json"}) return Response(encode({"error": e.message}), 500, {"Content-Type": "application/json"})

View File

@ -0,0 +1,10 @@
class InvalidAttributeException(Exception):
def __init__(self, errors):
self.errors = errors
def get_error_message(self):
messages = {}
for error in self.errors:
messages[error.path[0]] = error.msg
return messages

View File

@ -22,11 +22,11 @@ from pkg_resources import get_distribution
from almanach.common.almanach_entity_not_found_exception import AlmanachEntityNotFoundException from almanach.common.almanach_entity_not_found_exception import AlmanachEntityNotFoundException
from almanach.common.date_format_exception import DateFormatException from almanach.common.date_format_exception import DateFormatException
from almanach.core.model import Instance, Volume, VolumeType from almanach.core.model import Instance, Volume, VolumeType
from almanach.validators.instance_validator import InstanceValidator
from almanach import config from almanach import config
class Controller(object): class Controller(object):
def __init__(self, database_adapter): def __init__(self, database_adapter):
self.database_adapter = database_adapter self.database_adapter = database_adapter
self.metadata_whitelist = config.device_metadata_whitelist() self.metadata_whitelist = config.device_metadata_whitelist()
@ -107,12 +107,11 @@ class Controller(object):
instance.last_event = rebuild_date instance.last_event = rebuild_date
self.database_adapter.insert_entity(instance) self.database_adapter.insert_entity(instance)
def update_active_instance_entity(self, instance_id, start_date): def update_active_instance_entity(self, instance_id, **kwargs):
try: try:
InstanceValidator().validate_update(kwargs)
instance = self.database_adapter.get_active_entity(instance_id) instance = self.database_adapter.get_active_entity(instance_id)
instance.start = self._validate_and_parse_date(start_date) self._update_instance_object(instance, **kwargs)
logging.info("Updating entity for instance '{0}' with a new start_date={1}".format(instance_id, start_date))
self.database_adapter.update_active_entity(instance) self.database_adapter.update_active_entity(instance)
return instance return instance
except KeyError as e: except KeyError as e:
@ -155,6 +154,20 @@ class Controller(object):
logging.error("Trying to detach a volume with id '%s' not in the database yet." % volume_id) logging.error("Trying to detach a volume with id '%s' not in the database yet." % volume_id)
raise e raise e
def _update_instance_object(self, instance, **kwargs):
for attribute, key in dict(start="start_date", end="end_date").items():
value = kwargs.get(key)
if value:
setattr(instance, attribute, self._validate_and_parse_date(value))
logging.info("Updating entity for instance '{0}' with {1}={2}".format(instance.entity_id, key, value))
for attribute in ["name", "flavor", "os", "metadata"]:
value = kwargs.get(attribute)
if value:
setattr(instance, attribute, value)
logging.info(
"Updating entity for instance '{0}' with {1}={2}".format(instance.entity_id, attribute, value))
def _volume_attach_instance(self, volume_id, date, attachments): def _volume_attach_instance(self, volume_id, date, attachments):
volume = self.database_adapter.get_active_entity(volume_id) volume = self.database_adapter.get_active_entity(volume_id)
date = self._localize_date(date) date = self._localize_date(date)

View File

View File

@ -0,0 +1,24 @@
from almanach.common.validation_exception import InvalidAttributeException
from voluptuous import Schema, MultipleInvalid, Datetime, Required
class InstanceValidator(object):
def __init__(self):
self.schema = Schema({
'name': unicode,
'flavor': unicode,
'os': {
Required('distro'): unicode,
Required('version'): unicode,
Required('os_type'): unicode,
},
'metadata': dict,
'start_date': Datetime(),
'end_date': Datetime(),
})
def validate_update(self, payload):
try:
return self.schema(payload)
except MultipleInvalid as e:
raise InvalidAttributeException(e.errors)

View File

@ -5,4 +5,5 @@ pymongo==2.7.2
kombu>=3.0.30 kombu>=3.0.30
python-dateutil==2.2 python-dateutil==2.2
python-pymongomodem==0.0.3 python-pymongomodem==0.0.3
pytz>=2014.10 pytz>=2014.10
voluptuous==0.8.11

View File

@ -6,5 +6,5 @@ nose-cov==1.6
nose-blockage==0.1.2 nose-blockage==0.1.2
flexmock==0.9.4 flexmock==0.9.4
mongomock==2.0.0 mongomock==2.0.0
PyHamcrest==1.8.1 PyHamcrest==1.8.5
flake8==2.5.4 flake8==2.5.4

View File

@ -60,6 +60,10 @@ class EntityBuilder(Builder):
self.dict_object["end"] = None self.dict_object["end"] = None
return self return self
def with_flavor(self, flavor):
self.dict_object["flavor"] = flavor
return self
def with_metadata(self, metadata): def with_metadata(self, metadata):
self.dict_object['metadata'] = metadata self.dict_object['metadata'] = metadata
return self return self

View File

@ -12,19 +12,27 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
import logging
import unittest import unittest
from datetime import datetime, timedelta
import pytz import pytz
from dateutil import parser as date_parser
from copy import copy
from datetime import datetime, timedelta
from dateutil.parser import parse
from hamcrest import raises, calling, assert_that
from flexmock import flexmock, flexmock_teardown from flexmock import flexmock, flexmock_teardown
from nose.tools import assert_raises from nose.tools import assert_raises
from tests.builder import a, instance, volume, volume_type
from almanach import config from almanach import config
from almanach.common.almanach_entity_not_found_exception import AlmanachEntityNotFoundException from almanach.common.almanach_entity_not_found_exception import AlmanachEntityNotFoundException
from almanach.common.date_format_exception import DateFormatException from almanach.common.date_format_exception import DateFormatException
from almanach.common.validation_exception import InvalidAttributeException
from almanach.core.controller import Controller from almanach.core.controller import Controller
from almanach.core.model import Instance, Volume from almanach.core.model import Instance, Volume
from tests.builder import a, instance, volume, volume_type
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
class ControllerTest(unittest.TestCase): class ControllerTest(unittest.TestCase):
@ -70,6 +78,9 @@ class ControllerTest(unittest.TestCase):
fake_instance = a(instance()) fake_instance = a(instance())
dates_str = "2015-10-21T16:25:00.000000Z" dates_str = "2015-10-21T16:25:00.000000Z"
fake_instance.start = parse(dates_str)
fake_instance.end = None
fake_instance.last_event = parse(dates_str)
(flexmock(self.database_adapter) (flexmock(self.database_adapter)
.should_receive("get_active_entity") .should_receive("get_active_entity")
@ -78,11 +89,9 @@ class ControllerTest(unittest.TestCase):
.once()) .once())
(flexmock(self.database_adapter) (flexmock(self.database_adapter)
.should_receive("close_active_entity") .should_receive("close_active_entity")
.with_args(fake_instance.entity_id, date_parser.parse(dates_str)) .with_args(fake_instance.entity_id, parse(dates_str))
.once()) .once())
fake_instance.start = dates_str
fake_instance.end = None
fake_instance.last_event = dates_str
(flexmock(self.database_adapter) (flexmock(self.database_adapter)
.should_receive("insert_entity") .should_receive("insert_entity")
.with_args(fake_instance) .with_args(fake_instance)
@ -90,27 +99,90 @@ class ControllerTest(unittest.TestCase):
self.controller.resize_instance(fake_instance.entity_id, "newly_flavor", dates_str) self.controller.resize_instance(fake_instance.entity_id, "newly_flavor", dates_str)
def test_update_active_instance_entity_with_a_new_flavor(self):
flavor = u"my flavor name"
fake_instance1 = a(instance())
fake_instance2 = copy(fake_instance1)
self._expect_get_active_entity_and_update(fake_instance1, fake_instance2, flavor=flavor)
self.controller.update_active_instance_entity(
instance_id=fake_instance1.entity_id,
flavor=flavor,
)
def test_update_active_instance_entity_with_a_new_name(self):
name = u"my instance name"
fake_instance1 = a(instance())
fake_instance2 = copy(fake_instance1)
self._expect_get_active_entity_and_update(fake_instance1, fake_instance2, name=name)
self.controller.update_active_instance_entity(
instance_id=fake_instance1.entity_id,
name=name,
)
def test_update_active_instance_entity_with_a_new_os(self):
os = {
"os_type": u"linux",
"version": u"7",
"distro": u"centos"
}
fake_instance1 = a(instance())
fake_instance2 = copy(fake_instance1)
self._expect_get_active_entity_and_update(fake_instance1, fake_instance2, os=os)
self.controller.update_active_instance_entity(
instance_id=fake_instance1.entity_id,
os=os,
)
def test_update_active_instance_entity_with_a_new_metadata(self):
metadata = {
"key": "value"
}
fake_instance1 = a(instance())
fake_instance2 = copy(fake_instance1)
self._expect_get_active_entity_and_update(fake_instance1, fake_instance2, metadata=metadata)
self.controller.update_active_instance_entity(
instance_id=fake_instance1.entity_id,
metadata=metadata,
)
def test_update_active_instance_entity_with_a_new_start_date(self): def test_update_active_instance_entity_with_a_new_start_date(self):
fake_instance1 = a(instance()) fake_instance1 = a(instance())
fake_instance2 = fake_instance1 fake_instance2 = copy(fake_instance1)
fake_instance2.start = "2015-10-21T16:25:00.000000Z" self._expect_get_active_entity_and_update(fake_instance1, fake_instance2, start="2015-10-21T16:25:00.000000Z")
(flexmock(self.database_adapter)
.should_receive("get_active_entity")
.with_args(fake_instance1.entity_id)
.and_return(fake_instance1)
.once())
(flexmock(self.database_adapter)
.should_receive("update_active_entity")
.with_args(fake_instance2)
.once())
self.controller.update_active_instance_entity( self.controller.update_active_instance_entity(
instance_id=fake_instance1.entity_id, instance_id=fake_instance1.entity_id,
start_date="2015-10-21T16:25:00.000000Z", start_date="2015-10-21T16:25:00.000000Z",
) )
def test_update_active_instance_entity_with_a_new_end_date(self):
fake_instance1 = a(instance())
fake_instance2 = copy(fake_instance1)
self._expect_get_active_entity_and_update(fake_instance1, fake_instance2, end="2015-10-21T16:25:00.000000Z")
self.controller.update_active_instance_entity(
instance_id=fake_instance1.entity_id,
end_date="2015-10-21T16:25:00.000000Z",
)
def test_instance_updated_wrong_attributes_raises_exception(self):
fake_instance1 = a(instance())
(flexmock(self.database_adapter)
.should_receive("get_active_entity")
.with_args(fake_instance1.entity_id)
.and_return(fake_instance1)
.never())
assert_that(
calling(self.controller.update_active_instance_entity).with_args(instance_id=fake_instance1.entity_id,
wrong_attribute="this is wrong"),
raises(InvalidAttributeException))
def test_instance_created_but_its_an_old_event(self): def test_instance_created_but_its_an_old_event(self):
fake_instance = a(instance() fake_instance = a(instance()
.with_last_event(pytz.utc.localize(datetime(2015, 10, 21, 16, 29, 0)))) .with_last_event(pytz.utc.localize(datetime(2015, 10, 21, 16, 29, 0))))
@ -158,7 +230,7 @@ class ControllerTest(unittest.TestCase):
(flexmock(self.database_adapter) (flexmock(self.database_adapter)
.should_receive("close_active_entity") .should_receive("close_active_entity")
.with_args("id1", date_parser.parse("2015-10-21T16:25:00.000000Z")) .with_args("id1", parse("2015-10-21T16:25:00.000000Z"))
.once()) .once())
self.controller.delete_instance("id1", "2015-10-21T16:25:00.000000Z") self.controller.delete_instance("id1", "2015-10-21T16:25:00.000000Z")
@ -384,6 +456,10 @@ class ControllerTest(unittest.TestCase):
def test_volume_updated(self): def test_volume_updated(self):
fake_volume = a(volume()) fake_volume = a(volume())
dates_str = "2015-10-21T16:25:00.000000Z" dates_str = "2015-10-21T16:25:00.000000Z"
fake_volume.size = "new_size"
fake_volume.start = parse(dates_str)
fake_volume.end = None
fake_volume.last_event = parse(dates_str)
(flexmock(self.database_adapter) (flexmock(self.database_adapter)
.should_receive("get_active_entity") .should_receive("get_active_entity")
@ -392,12 +468,9 @@ class ControllerTest(unittest.TestCase):
.once()) .once())
(flexmock(self.database_adapter) (flexmock(self.database_adapter)
.should_receive("close_active_entity") .should_receive("close_active_entity")
.with_args(fake_volume.entity_id, date_parser.parse(dates_str)) .with_args(fake_volume.entity_id, parse(dates_str))
.once()) .once())
fake_volume.size = "new_size"
fake_volume.start = dates_str
fake_volume.end = None
fake_volume.last_event = dates_str
(flexmock(self.database_adapter) (flexmock(self.database_adapter)
.should_receive("insert_entity") .should_receive("insert_entity")
.with_args(fake_volume) .with_args(fake_volume)
@ -648,3 +721,20 @@ class ControllerTest(unittest.TestCase):
.once()) .once())
self.assertEqual(len(self.controller.list_volume_types()), 2) self.assertEqual(len(self.controller.list_volume_types()), 2)
def _expect_get_active_entity_and_update(self, fake_instance1, fake_instance2, **kwargs):
for key, value in kwargs.items():
if key in ['start', 'end']:
value = parse(value)
setattr(fake_instance2, key, value)
(flexmock(self.database_adapter)
.should_receive("get_active_entity")
.with_args(fake_instance1.entity_id)
.and_return(fake_instance1)
.once())
(flexmock(self.database_adapter)
.should_receive("update_active_entity")
.with_args(fake_instance2)
.once())

View File

@ -18,8 +18,11 @@ import flask
from uuid import uuid4 from uuid import uuid4
from unittest import TestCase from unittest import TestCase
from datetime import datetime from datetime import datetime
from voluptuous import Invalid
from almanach.common.validation_exception import InvalidAttributeException
from flexmock import flexmock, flexmock_teardown from flexmock import flexmock, flexmock_teardown
from hamcrest import assert_that, has_key, equal_to, has_length, has_entry, has_entries from hamcrest import assert_that, has_key, equal_to, has_length, has_entry, has_entries, is_
from almanach import config from almanach import config
from almanach.common.date_format_exception import DateFormatException from almanach.common.date_format_exception import DateFormatException
@ -84,9 +87,9 @@ class ApiTest(TestCase):
.with_args( .with_args(
instance_id="INSTANCE_ID", instance_id="INSTANCE_ID",
start_date=data["start_date"], start_date=data["start_date"],
).and_return(a(instance().with_id('INSTANCE_ID'))) ).and_return(a(instance().with_id('INSTANCE_ID').with_start(2014, 01, 01, 00, 0, 00)))
code, result = self.api_update( code, result = self.api_put(
'/entity/instance/INSTANCE_ID', '/entity/instance/INSTANCE_ID',
headers={'X-Auth-Token': 'some token value'}, headers={'X-Auth-Token': 'some token value'},
data=data, data=data,
@ -96,6 +99,7 @@ class ApiTest(TestCase):
assert_that(result, has_key('entity_id')) assert_that(result, has_key('entity_id'))
assert_that(result, has_key('start')) assert_that(result, has_key('start'))
assert_that(result, has_key('end')) assert_that(result, has_key('end'))
assert_that(result['start'], is_("2014-01-01 00:00:00"))
def test_instances_with_wrong_authentication(self): def test_instances_with_wrong_authentication(self):
self.having_config('api_auth_token', 'some token value') self.having_config('api_auth_token', 'some token value')
@ -870,9 +874,6 @@ class ApiTest(TestCase):
def api_delete(self, url, query_string=None, data=None, headers=None, accept='application/json'): def api_delete(self, url, query_string=None, data=None, headers=None, accept='application/json'):
return self._api_call(url, "delete", data, query_string, headers, accept) return self._api_call(url, "delete", data, query_string, headers, accept)
def api_update(self, url, data=None, query_string=None, headers=None, accept='application/json'):
return self._api_call(url, "put", data, query_string, headers, accept)
def _api_call(self, url, method, data=None, query_string=None, headers=None, accept='application/json'): def _api_call(self, url, method, data=None, query_string=None, headers=None, accept='application/json'):
with self.app.test_client() as http_client: with self.app.test_client() as http_client:
if not headers: if not headers:
@ -890,6 +891,38 @@ class ApiTest(TestCase):
.should_receive(key) .should_receive(key)
.and_return(value)) .and_return(value))
def test_update_active_instance_entity_with_wrong_attribute_exception(self):
errors = [
Invalid(message="error message1", path=["my_attribute1"]),
Invalid(message="error message2", path=["my_attribute2"]),
]
formatted_errors = {
"my_attribute1": "error message1",
"my_attribute2": "error message2",
}
self.having_config('api_auth_token', 'some token value')
instance_id = 'INSTANCE_ID'
data = {
'flavor': 'A_FLAVOR',
}
self.controller.should_receive('update_active_instance_entity') \
.with_args(instance_id=instance_id, **data) \
.once() \
.and_raise(InvalidAttributeException(errors))
code, result = self.api_put(
'/entity/instance/INSTANCE_ID',
data=data,
headers={'X-Auth-Token': 'some token value'}
)
assert_that(result, has_entries({
"error": formatted_errors
}))
assert_that(code, equal_to(400))
class DateMatcher(object): class DateMatcher(object):

View File

View File

@ -0,0 +1,117 @@
import unittest
from almanach.common.validation_exception import InvalidAttributeException
from almanach.validators.instance_validator import InstanceValidator
from hamcrest import assert_that, calling, raises, is_
class InstanceValidatorTests(unittest.TestCase):
def test_validate_update_with_invalid_attribute(self):
instance_validator = InstanceValidator()
payload = {"invalid attribute": ".."}
assert_that(calling(instance_validator.validate_update).with_args(payload),
raises(InvalidAttributeException))
def test_validate_update_with_valid_name_attribute(self):
instance_validator = InstanceValidator()
payload = {"name": u"instance name"}
assert_that(instance_validator.validate_update(payload), is_(payload))
def test_validate_update_with_invalid_name_attribute(self):
instance_validator = InstanceValidator()
payload = {"name": 123}
assert_that(calling(instance_validator.validate_update).with_args(payload),
raises(InvalidAttributeException))
def test_validate_update_with_valid_flavor_attribute(self):
instance_validator = InstanceValidator()
payload = {"flavor": u"flavor"}
assert_that(instance_validator.validate_update(payload), is_(payload))
def test_validate_update_with_invalid_flavor_attribute(self):
instance_validator = InstanceValidator()
payload = {"flavor": 123}
assert_that(calling(instance_validator.validate_update).with_args(payload),
raises(InvalidAttributeException))
def test_validate_update_with_valid_start_date(self):
instance_validator = InstanceValidator()
payload = {"start_date": "2015-10-21T16:25:00.000000Z"}
assert_that(instance_validator.validate_update(payload),
is_(payload))
def test_validate_update_with_invalid_start_date(self):
instance_validator = InstanceValidator()
payload = {"start_date": "2015-10-21"}
assert_that(calling(instance_validator.validate_update).with_args(payload),
raises(InvalidAttributeException))
def test_validate_update_with_valid_end_date(self):
instance_validator = InstanceValidator()
payload = {"end_date": "2015-10-21T16:25:00.000000Z"}
assert_that(instance_validator.validate_update(payload),
is_(payload))
def test_validate_update_with_invalid_end_date(self):
instance_validator = InstanceValidator()
payload = {"end_date": "2016"}
assert_that(calling(instance_validator.validate_update).with_args(payload),
raises(InvalidAttributeException))
def test_validate_update_with_valid_os_attribute(self):
instance_validator = InstanceValidator()
payload = {
"os": {
"distro": u"centos",
"version": u"7",
"os_type": u"linux",
}
}
assert_that(instance_validator.validate_update(payload), is_(payload))
def test_validate_update_with_invalid_os_attribute(self):
instance_validator = InstanceValidator()
payload = {
"os": {
"distro": u"centos",
"os_type": u"linux",
}
}
assert_that(calling(instance_validator.validate_update).with_args(payload),
raises(InvalidAttributeException))
def test_validate_update_with_valid_metadata_attribute(self):
instance_validator = InstanceValidator()
payload = {
"metadata": {
"key": "value"
}
}
assert_that(instance_validator.validate_update(payload), is_(payload))
instance_validator = InstanceValidator()
payload = {
"metadata": {}
}
assert_that(instance_validator.validate_update(payload), is_(payload))
def test_validate_update_with_invalid_metadata_attribute(self):
instance_validator = InstanceValidator()
payload = {
"metadata": "foobar"
}
assert_that(calling(instance_validator.validate_update).with_args(payload),
raises(InvalidAttributeException))