Add unit tests for db documents api.

This commit is contained in:
Felipe Monteiro 2017-07-21 05:31:59 +01:00
parent bbd0292455
commit d50c9cef2e
10 changed files with 212 additions and 8 deletions

View File

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

View File

@ -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)