From d50c9cef2ebf8f9046794f355f9341df13830163 Mon Sep 17 00:00:00 2001 From: Felipe Monteiro Date: Fri, 21 Jul 2017 05:31:59 +0100 Subject: [PATCH] Add unit tests for db documents api. --- deckhand/common/__init__.py | 0 deckhand/common/timeutils.py | 88 ++++++++++++++++++++++++ deckhand/control/api.py | 3 +- deckhand/control/documents.py | 4 +- deckhand/db/sqlalchemy/api.py | 15 +++- deckhand/db/sqlalchemy/models.py | 9 +++ deckhand/tests/unit/base.py | 46 +++++++++++++ deckhand/tests/unit/control/test_api.py | 6 +- deckhand/tests/unit/db/__init__.py | 0 deckhand/tests/unit/db/test_documents.py | 49 +++++++++++++ 10 files changed, 212 insertions(+), 8 deletions(-) create mode 100644 deckhand/common/__init__.py create mode 100644 deckhand/common/timeutils.py create mode 100644 deckhand/tests/unit/base.py create mode 100644 deckhand/tests/unit/db/__init__.py create mode 100644 deckhand/tests/unit/db/test_documents.py diff --git a/deckhand/common/__init__.py b/deckhand/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/deckhand/common/timeutils.py b/deckhand/common/timeutils.py new file mode 100644 index 00000000..37a2eace --- /dev/null +++ b/deckhand/common/timeutils.py @@ -0,0 +1,88 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Time related utilities and helper functions. +""" + +import datetime + +import iso8601 +from monotonic import monotonic as now # noqa +from oslo_utils import encodeutils + +# ISO 8601 extended time format with microseconds +_ISO8601_TIME_FORMAT_SUBSECOND = '%Y-%m-%dT%H:%M:%S.%f' +_ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' +PERFECT_TIME_FORMAT = _ISO8601_TIME_FORMAT_SUBSECOND + + +def isotime(at=None, subsecond=False): + """Stringify time in ISO 8601 format.""" + if not at: + at = utcnow() + st = at.strftime(_ISO8601_TIME_FORMAT + if not subsecond + else _ISO8601_TIME_FORMAT_SUBSECOND) + tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' + st += ('Z' if tz == 'UTC' else tz) + return st + + +def parse_isotime(timestr): + """Parse time from ISO 8601 format.""" + try: + return iso8601.parse_date(timestr) + except iso8601.ParseError as e: + raise ValueError(encodeutils.exception_to_unicode(e)) + except TypeError as e: + raise ValueError(encodeutils.exception_to_unicode(e)) + + +def utcnow(with_timezone=False): + """Overridable version of utils.utcnow that can return a TZ-aware datetime. + """ + if utcnow.override_time: + try: + return utcnow.override_time.pop(0) + except AttributeError: + return utcnow.override_time + if with_timezone: + return datetime.datetime.now(tz=iso8601.iso8601.UTC) + return datetime.datetime.utcnow() + + +def normalize_time(timestamp): + """Normalize time in arbitrary timezone to UTC naive object.""" + offset = timestamp.utcoffset() + if offset is None: + return timestamp + return timestamp.replace(tzinfo=None) - offset + + +def iso8601_from_timestamp(timestamp, microsecond=False): + """Returns an iso8601 formatted date from timestamp.""" + return isotime(datetime.datetime.utcfromtimestamp(timestamp), microsecond) + +utcnow.override_time = None + + +def delta_seconds(before, after): + """Return the difference between two timing objects. + + Compute the difference in seconds between two date, time, or + datetime objects (as a float, to microsecond resolution). + """ + delta = after - before + return datetime.timedelta.total_seconds(delta) diff --git a/deckhand/control/api.py b/deckhand/control/api.py index 3a24067a..e18939e8 100644 --- a/deckhand/control/api.py +++ b/deckhand/control/api.py @@ -23,7 +23,6 @@ from deckhand.control import base as api_base from deckhand.control import documents from deckhand.control import secrets from deckhand.db.sqlalchemy import api as db_api -from deckhand.db.sqlalchemy import models as db_models CONF = cfg.CONF LOG = None @@ -54,7 +53,7 @@ def __setup_logging(): def __setup_db(): - db_models.register_models(db_api.get_engine()) + db_api.setup_db() def start_api(state_manager=None): diff --git a/deckhand/control/documents.py b/deckhand/control/documents.py index 99476abc..caae7b1d 100644 --- a/deckhand/control/documents.py +++ b/deckhand/control/documents.py @@ -19,6 +19,7 @@ import falcon from oslo_db import exception as db_exc from oslo_log import log as logging +from oslo_serialization import jsonutils as json from deckhand.control import base as api_base from deckhand.db.sqlalchemy import api as db_api @@ -63,13 +64,14 @@ class DocumentsResource(api_base.BaseResource): return self.return_error(resp, falcon.HTTP_400, message=e) try: - db_api.document_create(document) + created_document = db_api.document_create(document) except db_exc.DBDuplicateEntry as e: return self.return_error(resp, falcon.HTTP_409, message=e) except Exception as e: return self.return_error(resp, falcon.HTTP_500, message=e) resp.status = falcon.HTTP_201 + resp.body = json.dumps(created_document) def _check_document_exists(self): pass diff --git a/deckhand/db/sqlalchemy/api.py b/deckhand/db/sqlalchemy/api.py index e1d1ce7e..8054212d 100644 --- a/deckhand/db/sqlalchemy/api.py +++ b/deckhand/db/sqlalchemy/api.py @@ -20,6 +20,7 @@ import threading from oslo_config import cfg from oslo_db import exception as db_exception +from oslo_db import options from oslo_db.sqlalchemy import session from oslo_log import log as logging from oslo_utils import excutils @@ -39,6 +40,8 @@ LOG = logging.getLogger(__name__) CONF = cfg.CONF +options.set_defaults(CONF) + _FACADE = None _LOCK = threading.Lock() @@ -95,6 +98,14 @@ def clear_db_env(): _FACADE = None +def setup_db(): + models.register_models(get_engine()) + + +def drop_db(): + models.unregister_models(get_engine()) + + def document_create(values, session=None): """Create a document.""" values = values.copy() @@ -102,7 +113,9 @@ def document_create(values, session=None): values['schema_version'] = values.pop('schemaVersion') session = session or get_session() + document = models.Document() with session.begin(): - document = models.Document() document.update(values) document.save(session=session) + + return document.to_dict() diff --git a/deckhand/db/sqlalchemy/models.py b/deckhand/db/sqlalchemy/models.py index f0d4d4f6..e8e2a868 100644 --- a/deckhand/db/sqlalchemy/models.py +++ b/deckhand/db/sqlalchemy/models.py @@ -15,6 +15,7 @@ import uuid from oslo_db.sqlalchemy import models +from oslo_log import log as logging from oslo_serialization import jsonutils as json from oslo_utils import timeutils from sqlalchemy import Boolean @@ -28,6 +29,9 @@ from sqlalchemy import String from sqlalchemy import Text from sqlalchemy.types import TypeDecorator +from deckhand.common import timeutils + +LOG = logging.getLogger(__name__) # Declarative base class which maintains a catalog of classes and tables # relative to that base. @@ -96,6 +100,11 @@ class DeckhandBase(models.ModelBase, models.TimestampMixin): # Remove private state instance, as it is not serializable and causes # CircularReference. d.pop("_sa_instance_state") + + for k in ["created_at", "updated_at", "deleted_at", "deleted"]: + if k in d and d[k]: + d[k] = d[k].isoformat() + return d diff --git a/deckhand/tests/unit/base.py b/deckhand/tests/unit/base.py new file mode 100644 index 00000000..7350af00 --- /dev/null +++ b/deckhand/tests/unit/base.py @@ -0,0 +1,46 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import fixtures +from oslo_config import cfg +from oslo_log import log as logging +import testtools + +from deckhand.conf import config +from deckhand.db.sqlalchemy import api as db_api +from deckhand.db.sqlalchemy import models as db_models + +CONF = cfg.CONF +logging.register_options(CONF) +logging.setup(CONF, 'deckhand') + + +class DeckhandTestCase(testtools.TestCase): + + def setUp(self): + super(DeckhandTestCase, self).setUp() + self.useFixture(fixtures.FakeLogger('deckhand')) + + def override_config(self, name, override, group=None): + CONF.set_override(name, override, group) + self.addCleanup(CONF.clear_override, name, group) + + +class DeckhandWithDBTestCase(DeckhandTestCase): + + def setUp(self): + super(DeckhandWithDBTestCase, self).setUp() + self.override_config('connection', "sqlite://", group='database') + db_api.setup_db() + self.addCleanup(db_api.drop_db) diff --git a/deckhand/tests/unit/control/test_api.py b/deckhand/tests/unit/control/test_api.py index 854d5729..22a4bf47 100644 --- a/deckhand/tests/unit/control/test_api.py +++ b/deckhand/tests/unit/control/test_api.py @@ -23,13 +23,12 @@ from deckhand.control import base as api_base class TestApi(testtools.TestCase): @mock.patch.object(api, 'db_api', autospec=True) - @mock.patch.object(api, 'db_models', autospec=True) @mock.patch.object(api, 'config', autospec=True) @mock.patch.object(api, 'secrets', autospec=True) @mock.patch.object(api, 'documents', autospec=True) @mock.patch.object(api, 'falcon', autospec=True) def test_start_api(self, mock_falcon, mock_documents, mock_secrets, - mock_config, mock_db_models, mock_db_api): + mock_config, mock_db_api): mock_falcon_api = mock_falcon.API.return_value result = api.start_api() @@ -43,5 +42,4 @@ class TestApi(testtools.TestCase): mock.call('/api/v1.0/secrets', mock_secrets.SecretsResource()) ]) mock_config.parse_args.assert_called_once_with() - mock_db_models.register_models.assert_called_once_with( - mock_db_api.get_engine()) + mock_db_api.setup_db.assert_called_once_with() diff --git a/deckhand/tests/unit/db/__init__.py b/deckhand/tests/unit/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/deckhand/tests/unit/db/test_documents.py b/deckhand/tests/unit/db/test_documents.py new file mode 100644 index 00000000..4c72a960 --- /dev/null +++ b/deckhand/tests/unit/db/test_documents.py @@ -0,0 +1,49 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock + +import testtools + +from deckhand.db.sqlalchemy import api as db_api +from deckhand.tests.unit import base + + +class DocumentFixture(object): + + def get_minimal_fixture(self, **kwargs): + fixture = {'data': 'fake document data', + 'metadata': 'fake meta', + 'kind': 'FakeConfigType', + 'schemaVersion': 'deckhand/v1'} + fixture.update(kwargs) + return fixture + + +class TestDocumentsApi(base.DeckhandWithDBTestCase): + + def _validate_document(self, expected, actual): + expected['doc_metadata'] = expected.pop('metadata') + expected['schema_version'] = expected.pop('schemaVersion') + + # TODO: Validate "status" fields, like created_at. + self.assertIsInstance(actual, dict) + for key, val in expected.items(): + self.assertIn(key, actual) + self.assertEqual(val, actual[key]) + + def test_create_document(self): + fixture = DocumentFixture().get_minimal_fixture() + document = db_api.document_create(fixture) + self._validate_document(fixture, document)