From 3e58e2c1e8af1863bac4a557da6043de8e3d83ac Mon Sep 17 00:00:00 2001 From: Aleksey Ripinen Date: Fri, 16 Jan 2015 15:04:18 +0300 Subject: [PATCH] Convert db exceptions to api exceptions All handles of db exceptions were convert to api exceptions. Now clients don't receive error messages from database. Change-Id: Iab8fb03697964c876907fa2fab9e833247bc9525 --- storyboard/api/v1/project_groups.py | 9 + storyboard/api/v1/projects.py | 9 + storyboard/api/v1/stories.py | 7 + storyboard/api/v1/subscription_events.py | 4 + storyboard/api/v1/subscriptions.py | 5 + storyboard/api/v1/tasks.py | 7 + storyboard/api/v1/teams.py | 10 + storyboard/api/v1/timeline.py | 9 + storyboard/api/v1/user_preferences.py | 3 + storyboard/api/v1/user_tokens.py | 6 + storyboard/api/v1/users.py | 6 + storyboard/common/decorators.py | 30 +++ storyboard/common/exception.py | 204 +++++++++++++++++++-- storyboard/db/api/access_tokens.py | 5 +- storyboard/db/api/base.py | 171 +++++++++++++---- storyboard/db/api/projects.py | 7 +- storyboard/db/api/stories.py | 4 +- storyboard/db/api/tasks.py | 7 +- storyboard/db/api/users.py | 4 +- storyboard/tests/api/test_db_exceptions.py | 65 +++++++ storyboard/tests/db/test_db_exceptions.py | 77 ++++++++ 21 files changed, 589 insertions(+), 60 deletions(-) create mode 100644 storyboard/common/decorators.py create mode 100644 storyboard/tests/api/test_db_exceptions.py create mode 100644 storyboard/tests/db/test_db_exceptions.py diff --git a/storyboard/api/v1/project_groups.py b/storyboard/api/v1/project_groups.py index c70e9bce..49a071f4 100644 --- a/storyboard/api/v1/project_groups.py +++ b/storyboard/api/v1/project_groups.py @@ -24,6 +24,7 @@ import wsmeext.pecan as wsme_pecan import storyboard.api.auth.authorization_checks as checks from storyboard.api.v1 import validations from storyboard.api.v1 import wmodels +from storyboard.common import decorators import storyboard.common.exception as exc from storyboard.db.api import project_groups from storyboard.db.api import projects @@ -38,6 +39,7 @@ class ProjectsSubcontroller(rest.RestController): Project Group. """ + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.Project], int) def get(self, project_group_id): @@ -55,6 +57,7 @@ class ProjectsSubcontroller(rest.RestController): return [wmodels.Project.from_db_model(project) for project in project_group.projects] + @decorators.db_exceptions @secure(checks.superuser) @wsme_pecan.wsexpose(wmodels.Project, int, int) def put(self, project_group_id, project_id): @@ -65,6 +68,7 @@ class ProjectsSubcontroller(rest.RestController): return wmodels.Project.from_db_model(projects.project_get(project_id)) + @decorators.db_exceptions @secure(checks.superuser) @wsme_pecan.wsexpose(None, int, int) def delete(self, project_group_id, project_id): @@ -87,6 +91,7 @@ class ProjectGroupsController(rest.RestController): validation_post_schema = validations.PROJECT_GROUPS_POST_SCHEMA validation_put_schema = validations.PROJECT_GROUPS_PUT_SCHEMA + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose(wmodels.ProjectGroup, int) def get_one(self, project_group_id): @@ -103,6 +108,7 @@ class ProjectGroupsController(rest.RestController): return wmodels.ProjectGroup.from_db_model(group) + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.ProjectGroup], int, int, unicode, unicode, unicode, unicode) @@ -135,6 +141,7 @@ class ProjectGroupsController(rest.RestController): return [wmodels.ProjectGroup.from_db_model(group) for group in groups] + @decorators.db_exceptions @secure(checks.superuser) @wsme_pecan.wsexpose(wmodels.ProjectGroup, body=wmodels.ProjectGroup) def post(self, project_group): @@ -151,6 +158,7 @@ class ProjectGroupsController(rest.RestController): return wmodels.ProjectGroup.from_db_model(created_group) + @decorators.db_exceptions @secure(checks.superuser) @wsme_pecan.wsexpose(wmodels.ProjectGroup, int, body=wmodels.ProjectGroup) def put(self, project_group_id, project_group): @@ -170,6 +178,7 @@ class ProjectGroupsController(rest.RestController): return wmodels.ProjectGroup.from_db_model(updated_group) + @decorators.db_exceptions @secure(checks.superuser) @wsme_pecan.wsexpose(None, int) def delete(self, project_group_id): diff --git a/storyboard/api/v1/projects.py b/storyboard/api/v1/projects.py index c25526a5..3e9cecec 100644 --- a/storyboard/api/v1/projects.py +++ b/storyboard/api/v1/projects.py @@ -25,9 +25,11 @@ from storyboard.api.auth import authorization_checks as checks from storyboard.api.v1.search import search_engine from storyboard.api.v1 import validations from storyboard.api.v1 import wmodels +from storyboard.common import decorators from storyboard.db.api import projects as projects_api from storyboard.openstack.common.gettextutils import _ # noqa + CONF = cfg.CONF SEARCH_ENGINE = search_engine.get_engine() @@ -44,6 +46,7 @@ class ProjectsController(rest.RestController): validation_post_schema = validations.PROJECTS_POST_SCHEMA validation_put_schema = validations.PROJECTS_PUT_SCHEMA + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose(wmodels.Project, int) def get_one_by_id(self, project_id): @@ -60,6 +63,7 @@ class ProjectsController(rest.RestController): raise ClientSideError(_("Project %s not found") % project_id, status_code=404) + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose(wmodels.Project, unicode) def get_one_by_name(self, project_name): @@ -76,6 +80,7 @@ class ProjectsController(rest.RestController): raise ClientSideError(_("Project %s not found") % project_name, status_code=404) + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.Project], int, int, unicode, unicode, int, unicode, unicode) @@ -121,6 +126,7 @@ class ProjectsController(rest.RestController): return [wmodels.Project.from_db_model(p) for p in projects] + @decorators.db_exceptions @secure(checks.superuser) @wsme_pecan.wsexpose(wmodels.Project, body=wmodels.Project) def post(self, project): @@ -128,9 +134,11 @@ class ProjectsController(rest.RestController): :param project: a project within the request body. """ + result = projects_api.project_create(project.as_dict()) return wmodels.Project.from_db_model(result) + @decorators.db_exceptions @secure(checks.superuser) @wsme_pecan.wsexpose(wmodels.Project, int, body=wmodels.Project) def put(self, project_id, project): @@ -155,6 +163,7 @@ class ProjectsController(rest.RestController): except ValueError: return False + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.Project], unicode, unicode, int, int) def search(self, q="", marker=None, limit=None): diff --git a/storyboard/api/v1/stories.py b/storyboard/api/v1/stories.py index 59d11b85..47596ea6 100644 --- a/storyboard/api/v1/stories.py +++ b/storyboard/api/v1/stories.py @@ -28,6 +28,7 @@ from storyboard.api.v1.timeline import CommentsController from storyboard.api.v1.timeline import TimeLineEventsController from storyboard.api.v1 import validations from storyboard.api.v1 import wmodels +from storyboard.common import decorators from storyboard.db.api import stories as stories_api from storyboard.db.api import timeline_events as events_api from storyboard.openstack.common.gettextutils import _ # noqa @@ -46,6 +47,7 @@ class StoriesController(rest.RestController): validation_post_schema = validations.STORIES_POST_SCHEMA validation_put_schema = validations.STORIES_PUT_SCHEMA + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose(wmodels.Story, int) def get_one(self, story_id): @@ -61,6 +63,7 @@ class StoriesController(rest.RestController): raise ClientSideError(_("Story %s not found") % story_id, status_code=404) + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.Story], unicode, unicode, [unicode], int, int, int, int, int, unicode, unicode) @@ -115,6 +118,7 @@ class StoriesController(rest.RestController): return [wmodels.Story.from_db_model(s) for s in stories] + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(wmodels.Story, body=wmodels.Story) def post(self, story): @@ -132,6 +136,7 @@ class StoriesController(rest.RestController): return wmodels.Story.from_db_model(created_story) + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(wmodels.Story, int, body=wmodels.Story) def put(self, story_id, story): @@ -154,6 +159,7 @@ class StoriesController(rest.RestController): raise ClientSideError(_("Story %s not found") % story_id, status_code=404) + @decorators.db_exceptions @secure(checks.superuser) @wsme_pecan.wsexpose(wmodels.Story, int) def delete(self, story_id): @@ -168,6 +174,7 @@ class StoriesController(rest.RestController): comments = CommentsController() events = TimeLineEventsController() + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.Story], unicode, unicode, int, int) def search(self, q="", marker=None, limit=None): diff --git a/storyboard/api/v1/subscription_events.py b/storyboard/api/v1/subscription_events.py index 181375ee..df502ae4 100644 --- a/storyboard/api/v1/subscription_events.py +++ b/storyboard/api/v1/subscription_events.py @@ -24,6 +24,7 @@ import wsmeext.pecan as wsme_pecan from storyboard.api.auth import authorization_checks as checks from storyboard.api.v1 import base +from storyboard.common import decorators from storyboard.db.api import subscription_events as subscription_events_api from storyboard.db.api import users as user_api from storyboard.openstack.common.gettextutils import _ # noqa @@ -69,6 +70,7 @@ class SubscriptionEventsController(rest.RestController): subscriptionEvents. """ + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(SubscriptionEvent, int) def get_one(self, subscription_event_id): @@ -86,6 +88,7 @@ class SubscriptionEventsController(rest.RestController): return SubscriptionEvent.from_db_model(subscription_event) + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose([SubscriptionEvent], int, int, unicode, int, unicode, unicode) @@ -136,6 +139,7 @@ class SubscriptionEventsController(rest.RestController): return [SubscriptionEvent.from_db_model(s) for s in subscriptions] + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(None, int) def delete(self, subscription_event_id): diff --git a/storyboard/api/v1/subscriptions.py b/storyboard/api/v1/subscriptions.py index e7659235..e2e3bfab 100644 --- a/storyboard/api/v1/subscriptions.py +++ b/storyboard/api/v1/subscriptions.py @@ -24,6 +24,7 @@ import wsmeext.pecan as wsme_pecan from storyboard.api.auth import authorization_checks as checks from storyboard.api.v1 import base +from storyboard.common import decorators from storyboard.db.api import subscriptions as subscription_api from storyboard.db.api import users as user_api from storyboard.openstack.common.gettextutils import _ # noqa @@ -62,6 +63,7 @@ class SubscriptionsController(rest.RestController): Provides Create, Delete, and search methods for resource subscriptions. """ + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(Subscription, int) def get_one(self, subscription_id): @@ -79,6 +81,7 @@ class SubscriptionsController(rest.RestController): return Subscription.from_db_model(subscription) + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose([Subscription], int, int, [unicode], int, int, unicode, unicode) @@ -130,6 +133,7 @@ class SubscriptionsController(rest.RestController): return [Subscription.from_db_model(s) for s in subscriptions] + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(Subscription, body=Subscription) def post(self, subscription): @@ -171,6 +175,7 @@ class SubscriptionsController(rest.RestController): result = subscription_api.subscription_create(subscription.as_dict()) return Subscription.from_db_model(result) + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(None, int) def delete(self, subscription_id): diff --git a/storyboard/api/v1/tasks.py b/storyboard/api/v1/tasks.py index f6f49fe7..ed329425 100644 --- a/storyboard/api/v1/tasks.py +++ b/storyboard/api/v1/tasks.py @@ -25,6 +25,7 @@ from storyboard.api.auth import authorization_checks as checks from storyboard.api.v1.search import search_engine from storyboard.api.v1 import validations from storyboard.api.v1 import wmodels +from storyboard.common import decorators from storyboard.db.api import tasks as tasks_api from storyboard.db.api import timeline_events as events_api from storyboard.openstack.common.gettextutils import _ # noqa @@ -42,6 +43,7 @@ class TasksController(rest.RestController): validation_post_schema = validations.TASKS_POST_SCHEMA validation_put_schema = validations.TASKS_PUT_SCHEMA + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose(wmodels.Task, int) def get_one(self, task_id): @@ -57,6 +59,7 @@ class TasksController(rest.RestController): raise ClientSideError(_("Task %s not found") % task_id, status_code=404) + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.Task], unicode, int, int, int, int, [unicode], [unicode], int, int, unicode, unicode) @@ -116,6 +119,7 @@ class TasksController(rest.RestController): return [wmodels.Task.from_db_model(s) for s in tasks] + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(wmodels.Task, body=wmodels.Task) def post(self, task): @@ -136,6 +140,7 @@ class TasksController(rest.RestController): return wmodels.Task.from_db_model(created_task) + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(wmodels.Task, int, body=wmodels.Task) def put(self, task_id, task): @@ -200,6 +205,7 @@ class TasksController(rest.RestController): task_title=original_task.title, author_id=author_id) + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(wmodels.Task, int) def delete(self, task_id): @@ -219,6 +225,7 @@ class TasksController(rest.RestController): response.status_code = 204 + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.Task], unicode, unicode, int, int) def search(self, q="", marker=None, limit=None): diff --git a/storyboard/api/v1/teams.py b/storyboard/api/v1/teams.py index 7c5d5c2c..e22795a0 100644 --- a/storyboard/api/v1/teams.py +++ b/storyboard/api/v1/teams.py @@ -25,6 +25,7 @@ import wsmeext.pecan as wsme_pecan from storyboard.api.auth import authorization_checks as checks from storyboard.api.v1 import validations from storyboard.api.v1 import wmodels +from storyboard.common import decorators from storyboard.common import exception as exc from storyboard.db.api import teams as teams_api from storyboard.db.api import users as users_api @@ -36,6 +37,7 @@ CONF = cfg.CONF class UsersSubcontroller(rest.RestController): """This controller should be used to list, add or remove users from a Team. """ + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.User], int) def get(self, team_id): @@ -51,6 +53,7 @@ class UsersSubcontroller(rest.RestController): return [wmodels.User.from_db_model(user) for user in team.users] + @decorators.db_exceptions @secure(checks.superuser) @wsme_pecan.wsexpose(wmodels.User, int, int) def put(self, team_id, user_id): @@ -61,6 +64,7 @@ class UsersSubcontroller(rest.RestController): return wmodels.User.from_db_model(user) + @decorators.db_exceptions @secure(checks.superuser) @wsme_pecan.wsexpose(None, int, int) def delete(self, team_id, user_id): @@ -76,6 +80,7 @@ class TeamsController(rest.RestController): validation_post_schema = validations.TEAMS_POST_SCHEMA validation_put_schema = validations.TEAMS_PUT_SCHEMA + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose(wmodels.Team, int) def get_one_by_id(self, team_id): @@ -92,6 +97,7 @@ class TeamsController(rest.RestController): raise ClientSideError(_("Team %s not found") % team_id, status_code=404) + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose(wmodels.Team, unicode) def get_one_by_name(self, team_name): @@ -108,6 +114,7 @@ class TeamsController(rest.RestController): raise ClientSideError(_("Team %s not found") % team_name, status_code=404) + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.Team], int, int, unicode, unicode, unicode, unicode) @@ -148,6 +155,7 @@ class TeamsController(rest.RestController): return [wmodels.Team.from_db_model(t) for t in teams] + @decorators.db_exceptions @secure(checks.superuser) @wsme_pecan.wsexpose(wmodels.Team, body=wmodels.Team) def post(self, team): @@ -158,6 +166,7 @@ class TeamsController(rest.RestController): result = teams_api.team_create(team.as_dict()) return wmodels.Team.from_db_model(result) + @decorators.db_exceptions @secure(checks.superuser) @wsme_pecan.wsexpose(wmodels.Team, int, body=wmodels.Team) def put(self, team_id, team): @@ -203,6 +212,7 @@ class TeamsController(rest.RestController): # Use default routing for all other requests return super(TeamsController, self)._route(args, request) + @decorators.db_exceptions @secure(checks.superuser) @wsme_pecan.wsexpose(None, int) def delete(self, team_id): diff --git a/storyboard/api/v1/timeline.py b/storyboard/api/v1/timeline.py index 8b409b2a..325df115 100644 --- a/storyboard/api/v1/timeline.py +++ b/storyboard/api/v1/timeline.py @@ -24,6 +24,7 @@ import wsmeext.pecan as wsme_pecan from storyboard.api.auth import authorization_checks as checks from storyboard.api.v1.search import search_engine from storyboard.api.v1 import wmodels +from storyboard.common import decorators from storyboard.common import event_types from storyboard.db.api import comments as comments_api from storyboard.db.api import timeline_events as events_api @@ -37,6 +38,7 @@ SEARCH_ENGINE = search_engine.get_engine() class TimeLineEventsController(rest.RestController): """Manages comments.""" + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose(wmodels.TimeLineEvent, int, int) def get_one(self, story_id, event_id): @@ -57,6 +59,7 @@ class TimeLineEventsController(rest.RestController): raise ClientSideError(_("Comment %s not found") % event_id, status_code=404) + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.TimeLineEvent], int, int, int, unicode, unicode) @@ -99,6 +102,7 @@ class TimeLineEventsController(rest.RestController): class CommentsController(rest.RestController): """Manages comments.""" + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose(wmodels.Comment, int, int) def get_one(self, story_id, comment_id): @@ -117,6 +121,7 @@ class CommentsController(rest.RestController): raise ClientSideError(_("Comment %s not found") % comment_id, status_code=404) + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.Comment], int, int, int, unicode, unicode) def get_all(self, story_id=None, marker=None, limit=None, sort_field='id', @@ -166,6 +171,7 @@ class CommentsController(rest.RestController): return [wmodels.Comment.from_db_model(comment) for comment in comments] + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(wmodels.TimeLineEvent, int, body=wmodels.Comment) def post(self, story_id, comment): @@ -188,6 +194,7 @@ class CommentsController(rest.RestController): event = wmodels.TimeLineEvent.resolve_event_values(event) return event + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(wmodels.Comment, int, int, body=wmodels.Comment) def put(self, story_id, comment_id, comment_body): @@ -210,6 +217,7 @@ class CommentsController(rest.RestController): return wmodels.Comment.from_db_model(updated_comment) + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(wmodels.Comment, int, int) def delete(self, story_id, comment_id): @@ -230,6 +238,7 @@ class CommentsController(rest.RestController): response.status_code = 204 return response + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.Comment], unicode, unicode, int, int) def search(self, q="", marker=None, limit=None): diff --git a/storyboard/api/v1/user_preferences.py b/storyboard/api/v1/user_preferences.py index 7e89bbfc..89a8ce05 100644 --- a/storyboard/api/v1/user_preferences.py +++ b/storyboard/api/v1/user_preferences.py @@ -23,6 +23,7 @@ import wsmeext.pecan as wsme_pecan from storyboard.api.auth import authorization_checks as checks from storyboard.api.v1 import validations +from storyboard.common import decorators import storyboard.db.api.users as user_api from storyboard.openstack.common.gettextutils import _ # noqa from storyboard.openstack.common import log @@ -35,6 +36,7 @@ LOG = log.getLogger(__name__) class UserPreferencesController(rest.RestController): validation_post_schema = validations.USER_PREFERENCES_POST_SCHEMA + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(types.DictType(unicode, unicode), int) def get_all(self, user_id): @@ -46,6 +48,7 @@ class UserPreferencesController(rest.RestController): return user_api.user_get_preferences(user_id) + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(types.DictType(unicode, unicode), int, body=types.DictType(unicode, unicode)) diff --git a/storyboard/api/v1/user_tokens.py b/storyboard/api/v1/user_tokens.py index b3ae9c95..a7d60b17 100644 --- a/storyboard/api/v1/user_tokens.py +++ b/storyboard/api/v1/user_tokens.py @@ -26,6 +26,7 @@ import wsmeext.pecan as wsme_pecan from storyboard.api.auth import authorization_checks as checks import storyboard.api.v1.wmodels as wmodels +from storyboard.common import decorators import storyboard.db.api.access_tokens as token_api import storyboard.db.api.users as user_api from storyboard.openstack.common.gettextutils import _ # noqa @@ -37,6 +38,7 @@ LOG = log.getLogger(__name__) class UserTokensController(rest.RestController): + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose([wmodels.AccessToken], int, int, int, unicode, unicode) @@ -77,6 +79,7 @@ class UserTokensController(rest.RestController): return [wmodels.AccessToken.from_db_model(t) for t in tokens] + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(wmodels.AccessToken, int, int) def get(self, user_id, access_token_id): @@ -94,6 +97,7 @@ class UserTokensController(rest.RestController): return wmodels.AccessToken.from_db_model(access_token) + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(wmodels.AccessToken, int, body=wmodels.AccessToken) def post(self, user_id, body): @@ -118,6 +122,7 @@ class UserTokensController(rest.RestController): return wmodels.AccessToken.from_db_model(token) + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(wmodels.AccessToken, int, int, body=wmodels.AccessToken) @@ -145,6 +150,7 @@ class UserTokensController(rest.RestController): return wmodels.AccessToken.from_db_model(result_token) + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(wmodels.AccessToken, int, int) def delete(self, user_id, access_token_id): diff --git a/storyboard/api/v1/users.py b/storyboard/api/v1/users.py index 5b735396..2cc313a2 100644 --- a/storyboard/api/v1/users.py +++ b/storyboard/api/v1/users.py @@ -28,6 +28,7 @@ from storyboard.api.v1.user_preferences import UserPreferencesController from storyboard.api.v1.user_tokens import UserTokensController from storyboard.api.v1 import validations from storyboard.api.v1 import wmodels +from storyboard.common import decorators from storyboard.db.api import users as users_api from storyboard.openstack.common.gettextutils import _ # noqa @@ -51,6 +52,7 @@ class UsersController(rest.RestController): validation_post_schema = validations.USERS_POST_SCHEMA validation_put_schema = validations.USERS_PUT_SCHEMA + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.User], int, int, unicode, unicode, unicode, unicode) @@ -90,6 +92,7 @@ class UsersController(rest.RestController): return [wmodels.User.from_db_model(u) for u in users] + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose(wmodels.User, int) def get_one(self, user_id): @@ -108,6 +111,7 @@ class UsersController(rest.RestController): status_code=404) return user + @decorators.db_exceptions @secure(checks.superuser) @wsme_pecan.wsexpose(wmodels.User, body=wmodels.User) def post(self, user): @@ -119,6 +123,7 @@ class UsersController(rest.RestController): created_user = users_api.user_create(user.as_dict()) return wmodels.User.from_db_model(created_user) + @decorators.db_exceptions @secure(checks.authenticated) @wsme_pecan.wsexpose(wmodels.User, int, body=wmodels.User) def put(self, user_id, user): @@ -155,6 +160,7 @@ class UsersController(rest.RestController): updated_user = users_api.user_update(user_id, user_dict) return wmodels.User.from_db_model(updated_user) + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.User], unicode, int, int) def search(self, q="", marker=None, limit=None): diff --git a/storyboard/common/decorators.py b/storyboard/common/decorators.py new file mode 100644 index 00000000..45f5ef4b --- /dev/null +++ b/storyboard/common/decorators.py @@ -0,0 +1,30 @@ +# Copyright (c) 2015 Mirantis Inc. +# +# 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 + +from pecan import abort + +from storyboard.common import exception as exc + + +def db_exceptions(func): + @functools.wraps(func) + def decorate(self, *args, **kwargs): + try: + return func(self, *args, **kwargs) + except exc.DBException as db_exc: + abort(db_exc.code, db_exc.message) + return decorate diff --git a/storyboard/common/exception.py b/storyboard/common/exception.py index 9765a607..3b8d8b65 100644 --- a/storyboard/common/exception.py +++ b/storyboard/common/exception.py @@ -4,7 +4,7 @@ # 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 +# 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, @@ -13,10 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from wsme.exc import ClientSideError + from storyboard.openstack.common.gettextutils import _ # noqa -class StoryboardException(Exception): +class StoryboardException(ClientSideError): """Base Exception for the project To correctly use this class, inherit from it and define @@ -28,8 +30,9 @@ class StoryboardException(Exception): def __str__(self): return self.message - def __init__(self): - super(StoryboardException, self).__init__(self.message) + def __init__(self, message=None, status_code=None): + super(StoryboardException, self).__init__(msg=message, + status_code=status_code) class NotFound(StoryboardException): @@ -40,17 +43,194 @@ class NotFound(StoryboardException): self.message = message -class DuplicateEntry(StoryboardException): - message = _("Database object already exists") - - def __init__(self, message=None): - if message: - self.message = message - - class NotEmpty(StoryboardException): message = _("Database object must be empty") def __init__(self, message=None): if message: self.message = message + + +class DBException(StoryboardException): + # Base exception for database errors + + message = _("Database Exception") + + def __init__(self, message=None, status_code=None): + """Constructor for base exception class + + :param: message: exception message. + :param: status_code: code of exception. + """ + if not status_code: + status_code = 400 + + super(DBException, self).__init__(message=message, + status_code=status_code) + + +class DBDuplicateEntry(DBException): + """Duplicate entry exception + + This exception wraps the same exception from database. + """ + + message = _("Database object already exists.") + + def __init__(self, message=None, object_name=None, value=None, + status_code=None): + """Constructor for duplicate entry exception + + :param : message: This message will be shown after exception raised. + :param : object_name: This parameter is name of object, in which + exception was raised. + :param: value: Invalid value. + :param: status_code: code of exception. + + If object_name or value is not 'None', to message will be appended with + new message with information about object name or invalid value + """ + + super(DBDuplicateEntry, self).__init__(message=message, + status_code=status_code) + db_message = None + + if object_name or value: + db_message_list = [_("Database object")] + + if object_name: + db_message_list.append(_("\'%s\'") % object_name) + + if value: + db_message_list.append(_("with field value \'%s\'") % value) + else: + db_message_list.append(_("with some of unique fields")) + + db_message_list.append(_("already exists.")) + db_message = _(" ").join(db_message_list) + + if db_message: + message_list = [] + + if message: + message_list.append(message) + + message_list.append(db_message) + self.msg = " ".join(message_list) + + +class DBConnectionError(DBException): + """Connection error exception + + This exception wraps the same exception from database. + """ + + message = _("Connection to database failed.") + + +class ColumnError(DBException): + """Column error exception + + This exception wraps the same exception from database. + """ + + message = _("Column is invalid or not found") + + +class DBDeadLock(DBException): + """Deadlock exception + + This exception wraps the same exception from database. + """ + + message = _("Database in dead lock") + + +class DBInvalidUnicodeParameter(DBException): + """Invalid unicode parameter exception + + This exception wraps the same exception from database. + """ + + message = _("Unicode parameter is passed to " + "a database without encoding directive") + + +class DBMigrationError(DBException): + """Migration error exception + + This exception wraps the same exception from database. + """ + + message = _("migrations could not be completed successfully") + + +class DBReferenceError(DBException): + """Reference error exception + + This exception wraps the same exception from database. + """ + + message = _("Foreign key error.") + + def __init__(self, message=None, object_name=None, value=None, + key=None, status_code=None): + """Constructor for duplicate entry exception + + :param : message: This message will be shown after exception raised. + :param : object_name: This parameter is name of object, in which + exception was raised. + :param: value: Invalid value. + :param : key: Field with invalid value. + :param : status_code: code of exception. + + If object_name or value or key is not 'None', to message will be + appended with new message with information about object name or + invalid value or field with invalid value. + """ + + super(DBReferenceError, self).__init__(message=message, + status_code=status_code) + db_message = None + + if object_name or value or key: + db_message_list = [] + + if object_name: + db_message_list.append( + _("Error in object")) + db_message_list.append(_("\'%s\'.") % object_name) + + if value or key: + db_message_list.append(_("Field")) + + if key: + db_message_list.append(_("\'%s\'") % key) + + if value: + db_message_list.append(_("value")) + db_message_list.append(_("\'%s\'") % value) + + db_message_list.append(_("is invalid.")) + + db_message = " ".join(db_message_list) + + if db_message: + message_list = [] + + if message: + message_list.append(message) + else: + message_list.append(self.message) + + message_list.append(db_message) + self.msg = " ".join(message_list) + + +class DBInvalidSortKey(DBException): + """Invalid sortkey error exception + + This exception wraps the same exception from database. + """ + + message = _("Invalid sort field") diff --git a/storyboard/db/api/access_tokens.py b/storyboard/db/api/access_tokens.py index 640cb3aa..e277a36f 100644 --- a/storyboard/db/api/access_tokens.py +++ b/storyboard/db/api/access_tokens.py @@ -18,6 +18,7 @@ import datetime from oslo.db.sqlalchemy.utils import InvalidSortKey from wsme.exc import ClientSideError +from storyboard.common import exception as exc from storyboard.db.api import base as api_base from storyboard.db import models from storyboard.openstack.common.gettextutils import _ # noqa @@ -56,8 +57,8 @@ def access_token_get_all(marker=None, limit=None, sort_field=None, marker=marker, sort_dir=sort_dir) except InvalidSortKey: - raise ClientSideError(_("Invalid sort_field [%s]") % (sort_field,), - status_code=400) + raise exc.DBInvalidSortKey( + _("Invalid sort_field [%s]") % (sort_field,)) except ValueError as ve: raise ClientSideError(_("%s") % (ve,), status_code=400) diff --git a/storyboard/db/api/base.py b/storyboard/db/api/base.py index 720e8375..09a8b922 100644 --- a/storyboard/db/api/base.py +++ b/storyboard/db/api/base.py @@ -40,8 +40,15 @@ def _get_facade_instance(): """Generate an instance of the DB Facade. """ global _FACADE - if _FACADE is None: - _FACADE = db_session.EngineFacade.from_config(CONF) + + try: + if _FACADE is None: + _FACADE = db_session.EngineFacade.from_config(CONF) + except db_exc.DBConnectionError: + raise exc.DBConnectionError() + except db_exc.DBDeadlock: + raise exc.DBDeadLock() + return _FACADE @@ -77,21 +84,37 @@ def get_engine(): """Returns the global instance of our database engine. """ facade = _get_facade_instance() - return facade.get_engine(use_slave=True) + + try: + return facade.get_engine(use_slave=True) + except db_exc.DBConnectionError: + raise exc.DBConnectionError() + except db_exc.DBDeadlock: + raise exc.DBDeadLock() def get_session(autocommit=True, expire_on_commit=False, **kwargs): """Returns a database session from our facade. """ facade = _get_facade_instance() - return facade.get_session(autocommit=autocommit, - expire_on_commit=expire_on_commit, **kwargs) + try: + return facade.get_session(autocommit=autocommit, + expire_on_commit=expire_on_commit, **kwargs) + except db_exc.DBConnectionError: + raise exc.DBConnectionError() + except db_exc.DBDeadlock: + raise exc.DBDeadLock() def cleanup(): """Manually clean up our database engine. """ - _destroy_facade_instance() + try: + _destroy_facade_instance() + except db_exc.DBConnectionError: + raise exc.DBConnectionError() + except db_exc.DBDeadlock: + raise exc.DBDeadLock() def model_query(model, session=None): @@ -100,13 +123,32 @@ def model_query(model, session=None): :param model: base model to query """ session = session or get_session() - query = session.query(model) + + try: + query = session.query(model) + except db_exc.DBConnectionError: + raise exc.DBConnectionError() + except db_exc.ColumnError: + raise exc.ColumnError() + except db_exc.DBDeadlock: + raise exc.DBDeadLock() + except db_exc.DBInvalidUnicodeParameter: + raise exc.DBInvalidUnicodeParameter() return query def __entity_get(kls, entity_id, session): - query = model_query(kls, session) - return query.filter_by(id=entity_id).first() + try: + query = model_query(kls, session) + return query.filter_by(id=entity_id).first() + except db_exc.DBConnectionError: + raise exc.DBConnectionError() + except db_exc.ColumnError: + raise exc.ColumnError() + except db_exc.DBDeadlock: + raise exc.DBDeadLock() + except db_exc.DBInvalidUnicodeParameter: + raise exc.DBInvalidUnicodeParameter() def entity_get(kls, entity_id, filter_non_public=False, session=None): @@ -123,7 +165,6 @@ def entity_get(kls, entity_id, filter_non_public=False, session=None): def entity_get_all(kls, filter_non_public=False, marker=None, limit=None, sort_field='id', sort_dir='asc', **kwargs): - # Sanity checks, in case someone accidentally explicitly passes in 'None' if not sort_field: sort_field = 'id' @@ -144,14 +185,21 @@ def entity_get_all(kls, filter_non_public=False, marker=None, limit=None, sort_keys=[sort_field], marker=marker, sort_dir=sort_dir) + + # Execute the query + entities = query.all() except InvalidSortKey: - raise ClientSideError(_("Invalid sort_field [%s]") % (sort_field,), - status_code=400) + raise exc.DBInvalidSortKey(_("Invalid sort_field [%s]") % + (sort_field,)) + except db_exc.DBConnectionError: + raise exc.DBConnectionError() + except db_exc.DBDeadlock: + raise exc.DBDeadLock() + except db_exc.DBInvalidUnicodeParameter: + raise exc.DBInvalidUnicodeParameter() except ValueError as ve: raise ClientSideError(_("%s") % (ve,), status_code=400) - # Execute the query - entities = query.all() if len(entities) > 0 and filter_non_public: sample_entity = entities[0] if len(entities) > 0 else None public_fields = getattr(sample_entity, "_public_fields", []) @@ -169,13 +217,21 @@ def entity_get_count(kls, **kwargs): # Sanity check on input parameters query = apply_query_filters(query=query, model=kls, **kwargs) - count = query.count() + try: + count = query.count() + except db_exc.DBConnectionError: + raise exc.DBConnectionError() + except db_exc.DBDeadlock: + raise exc.DBDeadLock() + except db_exc.DBInvalidUnicodeParameter: + raise exc.DBInvalidUnicodeParameter() return count def _filter_non_public_fields(entity, public_list=list()): ent_copy = copy.copy(entity) + for attr_name, val in six.iteritems(entity.__dict__): if attr_name.startswith("_"): continue @@ -191,12 +247,25 @@ def entity_create(kls, values): entity.update(values.copy()) session = get_session() - with session.begin(): - try: + + try: + with session.begin(): session.add(entity) - except db_exc.DBDuplicateEntry: - raise exc.DuplicateEntry(_("Duplicate entry for : %s") - % kls.__name__) + + except db_exc.DBDuplicateEntry as de: + raise exc.DBDuplicateEntry(object_name=kls.__name__, + value=de.value) + except db_exc.DBReferenceError as re: + raise exc.DBReferenceError(object_name=kls.__name__, + value=re.constraint, key=re.key) + except db_exc.DBConnectionError: + raise exc.DBConnectionError() + except db_exc.ColumnError: + raise exc.ColumnError() + except db_exc.DBDeadlock: + raise exc.DBDeadLock() + except db_exc.DBInvalidUnicodeParameter: + raise exc.DBInvalidUnicodeParameter return entity @@ -204,16 +273,32 @@ def entity_create(kls, values): def entity_update(kls, entity_id, values): session = get_session() - with session.begin(): - entity = __entity_get(kls, entity_id, session) - if entity is None: - raise exc.NotFound(_("%(name)s %(id)s not found") % - {'name': kls.__name__, 'id': entity_id}) + try: + with session.begin(): + entity = __entity_get(kls, entity_id, session) + if entity is None: + raise exc.NotFound(_("%(name)s %(id)s not found") % + {'name': kls.__name__, 'id': entity_id}) - values_copy = values.copy() - values_copy["id"] = entity_id - entity.update(values_copy) - session.add(entity) + values_copy = values.copy() + values_copy["id"] = entity_id + entity.update(values_copy) + session.add(entity) + + except db_exc.DBDuplicateEntry as de: + raise exc.DBDuplicateEntry(object_name=kls.__name__, + value=de.value) + except db_exc.DBReferenceError as re: + raise exc.DBReferenceError(object_name=kls.__name__, + value=re.constraint, key=re.key) + except db_exc.DBConnectionError: + raise exc.DBConnectionError() + except db_exc.ColumnError: + raise exc.ColumnError() + except db_exc.DBDeadlock: + raise exc.DBDeadLock() + except db_exc.DBInvalidUnicodeParameter: + raise exc.DBInvalidUnicodeParameter session = get_session() entity = __entity_get(kls, entity_id, session) @@ -223,11 +308,25 @@ def entity_update(kls, entity_id, values): def entity_hard_delete(kls, entity_id): session = get_session() - with session.begin(): - query = model_query(kls, session) - entity = query.filter_by(id=entity_id).first() - if entity is None: - raise exc.NotFound(_("%(name)s %(id)s not found") % - {'name': kls.__name__, 'id': entity_id}) - session.delete(entity) + try: + with session.begin(): + query = model_query(kls, session) + entity = query.filter_by(id=entity_id).first() + if entity is None: + raise exc.NotFound(_("%(name)s %(id)s not found") % + {'name': kls.__name__, 'id': entity_id}) + + session.delete(entity) + + except db_exc.DBReferenceError as re: + raise exc.DBReferenceError(object_name=kls.__name__, + value=re.constraint, key=re.key) + except db_exc.DBConnectionError: + raise exc.DBConnectionError() + except db_exc.ColumnError: + raise exc.ColumnError() + except db_exc.DBDeadlock: + raise exc.DBDeadLock() + except db_exc.DBInvalidUnicodeParameter: + raise exc.DBInvalidUnicodeParameter() diff --git a/storyboard/db/api/projects.py b/storyboard/db/api/projects.py index 9f47b9aa..7d8f9daa 100644 --- a/storyboard/db/api/projects.py +++ b/storyboard/db/api/projects.py @@ -4,7 +4,7 @@ # 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 +# 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, @@ -16,6 +16,7 @@ from oslo.db.sqlalchemy.utils import InvalidSortKey from wsme.exc import ClientSideError +from storyboard.common import exception as exc from storyboard.db.api import base as api_base from storyboard.db import models from storyboard.openstack.common.gettextutils import _ # noqa @@ -50,8 +51,8 @@ def project_get_all(marker=None, limit=None, sort_field=None, sort_dir=None, marker=marker, sort_dir=sort_dir) except InvalidSortKey: - raise ClientSideError(_("Invalid sort_field [%s]") % (sort_field,), - status_code=400) + raise exc.DBInvalidSortKey( + _("Invalid sort_field [%s]") % (sort_field,)) except ValueError as ve: raise ClientSideError(_("%s") % (ve,), status_code=400) diff --git a/storyboard/db/api/stories.py b/storyboard/db/api/stories.py index df67970d..904e1546 100644 --- a/storyboard/db/api/stories.py +++ b/storyboard/db/api/stories.py @@ -91,8 +91,8 @@ def story_get_all(title=None, description=None, status=None, assignee_id=None, marker=marker, sort_dir=sort_dir) except InvalidSortKey: - raise ClientSideError(_("Invalid sort_field [%s]") % (sort_field,), - status_code=400) + raise exc.DBInvalidSortKey( + _("Invalid sort_field [%s]") % (sort_field,)) except ValueError as ve: raise ClientSideError(_("%s") % (ve,), status_code=400) diff --git a/storyboard/db/api/tasks.py b/storyboard/db/api/tasks.py index d7535711..c0b113e4 100644 --- a/storyboard/db/api/tasks.py +++ b/storyboard/db/api/tasks.py @@ -4,7 +4,7 @@ # 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 +# 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, @@ -16,6 +16,7 @@ from oslo.db.sqlalchemy.utils import InvalidSortKey from wsme.exc import ClientSideError +from storyboard.common import exception as exc from storyboard.db.api import base as api_base from storyboard.db import models from storyboard.openstack.common.gettextutils import _ # noqa @@ -44,8 +45,8 @@ def task_get_all(marker=None, limit=None, sort_field=None, sort_dir=None, marker=marker, sort_dir=sort_dir) except InvalidSortKey: - raise ClientSideError(_("Invalid sort_field [%s]") % (sort_field,), - status_code=400) + raise exc.DBInvalidSortKey( + _("Invalid sort_field [%s]") % (sort_field,)) except ValueError as ve: raise ClientSideError("%s" % (ve,), status_code=400) diff --git a/storyboard/db/api/users.py b/storyboard/db/api/users.py index 7b1df4a5..1651efc1 100644 --- a/storyboard/db/api/users.py +++ b/storyboard/db/api/users.py @@ -73,8 +73,8 @@ def user_update_preferences(user_id, preferences): for key in preferences: value = preferences[key] prefs = api_base.entity_get_all(models.UserPreference, - user_id=user_id, - key=key) + user_id=user_id, + key=key) if prefs: pref = prefs[0] diff --git a/storyboard/tests/api/test_db_exceptions.py b/storyboard/tests/api/test_db_exceptions.py new file mode 100644 index 00000000..9c0ddba8 --- /dev/null +++ b/storyboard/tests/api/test_db_exceptions.py @@ -0,0 +1,65 @@ +# Copyright (c) 2015 Mirantis Inc. +# +# 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 json + +from storyboard.tests import base + + +class TestDBExceptions(base.FunctionalTest): + def setUp(self): + super(TestDBExceptions, self).setUp() + self.default_headers['Authorization'] = 'Bearer valid_superuser_token' + + # test duplicate entry error + # in this test we try to create two equal projects + def test_duplicate_project_create(self): + resource = '/projects' + project = { + 'name': 'test-project-duplicate', + 'description': 'test_project_duplicate_description', + } + + # create project with name 'test-project-duplicate' + response = self.post_json(resource, project) + body = json.loads(response.body) + self.assertEqual(project['name'], body['name']) + self.assertEqual(project['description'], body['description']) + + # repeat creating this project + # because project with name 'test-project-duplicate' already exists, we + # wait abort with code_status 400 + response = self.post_json(resource, project, expect_errors=True) + self.assertEqual(400, response.status_code) + + # test duplicate entry error + # in this test we try to create two equal users + def test_duplicate_user_create(self): + # send user first time + resource = '/users' + user = { + 'username': 'test_duplicate', + 'full_name': 'Test duplicate', + 'email': 'dupe@example.com' + } + + response = self.post_json(resource, user) + users_body = json.loads(response.body) + self.assertEqual(user['username'], users_body['username']) + self.assertEqual(user['full_name'], users_body['full_name']) + self.assertEqual(user['email'], users_body['email']) + + # send user again + response = self.post_json(resource, user, expect_errors=True) + self.assertEqual(400, response.status_code) diff --git a/storyboard/tests/db/test_db_exceptions.py b/storyboard/tests/db/test_db_exceptions.py new file mode 100644 index 00000000..797be079 --- /dev/null +++ b/storyboard/tests/db/test_db_exceptions.py @@ -0,0 +1,77 @@ +# Copyright (c) 2015 Mirantis Inc. +# +# 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 six + +from storyboard.common import exception as exc +from storyboard.db.api import projects +from storyboard.db.api import tasks +from storyboard.tests import base + + +class TestDBDuplicateEntry(base.DbTestCase): + def setUp(self): + super(TestDBDuplicateEntry, self).setUp() + + # create two projects with equal names + def test_users(self): + project = { + 'id': 10, + 'name': 'project', + 'description': 'Project 4 Description - foo' + } + + projects.project_create(project) + self.assertRaises(exc.DBDuplicateEntry, + lambda: projects.project_create(project)) + + +class TestDBReferenceError(base.DbTestCase): + def setUp(self): + super(TestDBReferenceError, self).setUp() + + # create task with id of not existing story + def test_teams(self): + task = { + 'id': 10, + 'story_id': 100 + } + + self.assertRaises(exc.DBReferenceError, + lambda: tasks.task_create(task)) + + +class TestDbInvalidSortKey(base.DbTestCase): + def setUp(self): + super(TestDbInvalidSortKey, self).setUp() + + # create project and sort his field with incorrect key + def test_projects(self): + project = { + 'id': 10, + 'name': 'testProject', + 'description': 'testProjectDescription' + } + + saved_project = projects.project_create(project) + self.assertIsNotNone(saved_project) + + for k, v in six.iteritems(project): + self.assertEqual(saved_project[k], v) + + self.assertRaises(exc.DBInvalidSortKey, + lambda: projects.project_get_all( + marker=10, + sort_field='invalid_sort_field'))