diff --git a/storyboard/common/__init__.py b/storyboard/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/storyboard/common/exception.py b/storyboard/common/exception.py new file mode 100644 index 00000000..fd1040ea --- /dev/null +++ b/storyboard/common/exception.py @@ -0,0 +1,46 @@ +# Copyright (c) 2014 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. + + +class StoryboardException(Exception): + """Base Exception for the project + + To correctly use this class, inherit from it and define + the 'message' property. + """ + + message = "An unknown exception occurred" + + def __str__(self): + return self.message + + def __init__(self): + super(StoryboardException, self).__init__(self.message) + + +class NotFound(StoryboardException): + message = "Object not found" + + def __init__(self, message=None): + if message: + self.message = message + + +class DuplicateEntry(StoryboardException): + message = "Database object already exists" + + def __init__(self, message=None): + if message: + self.message = message diff --git a/storyboard/db/api.py b/storyboard/db/api.py new file mode 100644 index 00000000..ba601c21 --- /dev/null +++ b/storyboard/db/api.py @@ -0,0 +1,125 @@ +# Copyright (c) 2014 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. + +from oslo.config import cfg + +from storyboard.common import exception as exc +from storyboard.db import models +from storyboard.openstack.common.db import exception as db_exc +from storyboard.openstack.common.db.sqlalchemy import session as db_session +from storyboard.openstack.common import log + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + +get_session = db_session.get_session + + +def model_query(model, session=None): + """Query helper. + + :param model: base model to query + """ + session = session or get_session() + query = session.query(model) + return query + + +## BEGIN Projects + +def _project_get(project_id, session): + query = model_query(models.Project, session) + return query.filter_by(id=project_id).first() + + +def project_get(project_id): + return _project_get(project_id, get_session()) + + +def project_get_all(**kwargs): + query = model_query(models.Project) + return query.filter_by(**kwargs).all() + + +def project_create(values): + project = models.Project() + project.update(values.copy()) + + session = get_session() + with session.begin(): + try: + project.save(session=session) + except db_exc.DBDuplicateEntry as e: + raise exc.DuplicateEntry("Duplicate entry for Project: %s" + % e.columns) + + return project + + +def project_update(project_id, values): + session = get_session() + + with session.begin(): + project = _project_get(project_id, session) + if project is None: + raise exc.NotFound("Project %s not found" % project_id) + + project.update(values.copy()) + + return project + + +## BEGIN Stories + +def _story_get(story_id, session): + query = model_query(models.Story, session) + return query.filter_by(id=story_id).first() + + +def story_get_all(**kwargs): + query = model_query(models.Story) + return query.filter_by(**kwargs).all() + + +def story_get(story_id): + return _story_get(story_id, get_session()) + + +def story_create(values): + story = models.Story() + story.update(values.copy()) + + session = get_session() + with session.begin(): + try: + story.save(session) + except db_exc.DBDuplicateEntry as e: + raise exc.DuplicateEntry("Duplicate etnry for Story: %s" + % e.colums) + + return story + + +def story_update(story_id, values): + session = get_session() + + with session.begin(): + story = _story_get(story_id, session) + if story is None: + raise exc.NotFound("Story %s not found" % story_id) + + story.update(values.copy()) + + return story diff --git a/storyboard/tests/base.py b/storyboard/tests/base.py index c0178960..f3e90410 100644 --- a/storyboard/tests/base.py +++ b/storyboard/tests/base.py @@ -101,7 +101,23 @@ class TestCase(testtools.TestCase): PATH_PREFIX = '/v1' -class FunctionalTest(TestCase): +class DbTestCase(TestCase): + + def setUp(self): + super(DbTestCase, self).setUp() + + CONF.set_override("connection", "sqlite://", "database") + self.init_db_cache() + + @lockutils.synchronized("storyboard", "db_init", True) + def init_db_cache(self): + global _DB_CACHE + if not _DB_CACHE: + _DB_CACHE = Database() + self.useFixture(_DB_CACHE) + + +class FunctionalTest(DbTestCase): """Used for functional tests of Pecan controllers where you need to test your literal application and its integration with the framework. @@ -110,18 +126,9 @@ class FunctionalTest(TestCase): def setUp(self): super(FunctionalTest, self).setUp() - CONF.set_override("connection", "sqlite://", "database") - self.init_db_cache() self.app = self._make_app() self.addCleanup(self._reset_pecan) - @lockutils.synchronized("storyboard", "db_init", True) - def init_db_cache(self): - global _DB_CACHE - if not _DB_CACHE: - _DB_CACHE = Database() - self.useFixture(_DB_CACHE) - def _make_app(self): config = { 'app': { @@ -152,7 +159,6 @@ class FunctionalTest(TestCase): :param path_prefix: prefix of the url path """ full_path = path_prefix + path - print('%s: %s %s' % (method.upper(), full_path, params)) response = getattr(self.app, "%s_json" % method)( str(full_path), params=params, @@ -161,7 +167,6 @@ class FunctionalTest(TestCase): extra_environ=extra_environ, expect_errors=expect_errors ) - print('GOT:%s' % response) return response def put_json(self, path, params, expect_errors=False, headers=None, @@ -232,13 +237,11 @@ class FunctionalTest(TestCase): :param path_prefix: prefix of the url path """ full_path = path_prefix + path - print('DELETE: %s' % (full_path)) response = self.app.delete(str(full_path), headers=headers, status=status, extra_environ=extra_environ, expect_errors=expect_errors) - print('GOT:%s' % response) return response def get_json(self, path, expect_errors=False, headers=None, @@ -268,7 +271,6 @@ class FunctionalTest(TestCase): all_params.update(params) if q: all_params.update(query_params) - print('GET: %s %r' % (full_path, all_params)) response = self.app.get(full_path, params=all_params, headers=headers, @@ -276,7 +278,6 @@ class FunctionalTest(TestCase): expect_errors=expect_errors) if not expect_errors: response = response.json - print('GOT:%s' % response) return response def validate_link(self, link): diff --git a/storyboard/tests/db/db_fixture.py b/storyboard/tests/db/db_fixture.py index bba0c6aa..ea808acf 100644 --- a/storyboard/tests/db/db_fixture.py +++ b/storyboard/tests/db/db_fixture.py @@ -67,6 +67,9 @@ class Database(fixtures.Fixture): def setUp(self): super(Database, self).setUp() - conn = self.engine.connect() + session.get_session() + engine = session.get_engine() + conn = engine.connect() + conn.connection.executescript(self._DB) - self.addCleanup(self.engine.dispose) + self.addCleanup(session.cleanup) diff --git a/storyboard/tests/db/test_db_api.py b/storyboard/tests/db/test_db_api.py new file mode 100644 index 00000000..2fd0fbd9 --- /dev/null +++ b/storyboard/tests/db/test_db_api.py @@ -0,0 +1,80 @@ +# Copyright (c) 2014 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. + +from storyboard.db import api as dbapi +from storyboard.tests import base + + +class ProjectsTest(base.DbTestCase): + + def setUp(self): + super(ProjectsTest, self).setUp() + + self.project_01 = { + 'name': u'StoryBoard', + 'description': u'Awesome Task Tracker' + } + + def test_save_project(self): + ref = self.project_01 + saved = dbapi.project_create(ref) + + self.assertIsNotNone(saved.id) + self.assertEqual(ref['name'], saved.name) + self.assertEqual(ref['description'], saved.description) + + def test_update_project(self): + saved = dbapi.project_create(self.project_01) + delta = { + 'name': u'New Name', + 'description': u'New Description' + } + updated = dbapi.project_update(saved.id, delta) + + self.assertEqual(saved.id, updated.id) + self.assertEqual(delta['name'], updated.name) + self.assertEqual(delta['description'], updated.description) + + +class StoriesTest(base.DbTestCase): + + def setUp(self): + super(StoriesTest, self).setUp() + + self.story_01 = { + 'title': u'Worst Story Ever', + 'description': u'Story description' + } + + def test_create_story(self): + ref = self.story_01 + saved = dbapi.story_create(self.story_01) + + self.assertIsNotNone(saved.id) + self.assertEqual(ref['title'], saved.title) + self.assertEqual(ref['description'], saved.description) + + def test_update_story(self): + saved = dbapi.story_create(self.story_01) + delta = { + 'title': u'New Title', + 'description': u'New Description' + } + + updated = dbapi.story_update(saved.id, delta) + + self.assertEqual(saved.id, updated.id) + self.assertEqual(delta['title'], updated.title) + self.assertEqual(delta['description'], updated.description)