Merge "Move SynchronousAPIBase to a generalized location"
This commit is contained in:
commit
5e9a14aee1
|
@ -34,9 +34,13 @@ import socket
|
|||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import time
|
||||
from unittest import mock
|
||||
import uuid
|
||||
|
||||
import fixtures
|
||||
import glance_store
|
||||
from os_win import utilsfactory as os_win_utilsfactory
|
||||
from oslo_config import cfg
|
||||
from oslo_serialization import jsonutils
|
||||
|
@ -44,8 +48,11 @@ from oslo_serialization import jsonutils
|
|||
from six.moves import range
|
||||
import six.moves.urllib.parse as urlparse
|
||||
import testtools
|
||||
import webob
|
||||
|
||||
from glance.common import config
|
||||
from glance.common import utils
|
||||
from glance.common import wsgi
|
||||
from glance.db.sqlalchemy import api as db_api
|
||||
from glance import tests as glance_tests
|
||||
from glance.tests import utils as test_utils
|
||||
|
@ -1467,3 +1474,209 @@ class MultipleBackendFunctionalTest(test_utils.BaseTestCase):
|
|||
self._attached_server_logs.append(s.log_file)
|
||||
self.addDetail(
|
||||
s.server_name, testtools.content.text_content(s.dump_log()))
|
||||
|
||||
|
||||
class SynchronousAPIBase(test_utils.BaseTestCase):
|
||||
"""A base class that provides synchronous calling into the API.
|
||||
|
||||
This provides a way to directly call into the API WSGI stack
|
||||
without starting a separate server, and with a simple paste
|
||||
pipeline. Configured with multi-store and a real database.
|
||||
|
||||
This differs from the FunctionalTest lineage above in that they
|
||||
start a full copy of the API server as a separate process, whereas
|
||||
this calls directly into the WSGI stack. This test base is
|
||||
appropriate for situations where you need to be able to mock the
|
||||
state of the world (i.e. warp time, or inject errors) but should
|
||||
not be used for happy-path testing where FunctionalTest provides
|
||||
more isolation.
|
||||
|
||||
To use this, inherit and run start_server() before you are ready
|
||||
to make API calls (either in your setUp() or per-test if you need
|
||||
to change config or mocking).
|
||||
|
||||
Once started, use the api_get(), api_put(), api_post(), and
|
||||
api_delete() methods to make calls to the API.
|
||||
|
||||
"""
|
||||
|
||||
TENANT = str(uuid.uuid4())
|
||||
|
||||
@mock.patch('oslo_db.sqlalchemy.enginefacade.writer.get_engine')
|
||||
def setup_database(self, mock_get_engine):
|
||||
"""Configure and prepare a fresh sqlite database."""
|
||||
db_file = 'sqlite:///%s/test.db' % self.test_dir
|
||||
self.config(connection=db_file, group='database')
|
||||
|
||||
# NOTE(danms): Make sure that we clear the current global
|
||||
# database configuration, provision a temporary database file,
|
||||
# and run migrations with our configuration to define the
|
||||
# schema there.
|
||||
db_api.clear_db_env()
|
||||
engine = db_api.get_engine()
|
||||
mock_get_engine.return_value = engine
|
||||
with mock.patch('logging.config'):
|
||||
# NOTE(danms): The alembic config in the env module will break our
|
||||
# BaseTestCase logging setup. So mock that out to prevent it while
|
||||
# we db_sync.
|
||||
test_utils.db_sync(engine=engine)
|
||||
|
||||
def setup_simple_paste(self):
|
||||
"""Setup a very simple no-auth paste pipeline.
|
||||
|
||||
This configures the API to be very direct, including only the
|
||||
middleware absolutely required for consistent API calls.
|
||||
"""
|
||||
self.paste_config = os.path.join(self.test_dir, 'glance-api-paste.ini')
|
||||
with open(self.paste_config, 'w') as f:
|
||||
f.write(textwrap.dedent("""
|
||||
[filter:context]
|
||||
paste.filter_factory = glance.api.middleware.context:\
|
||||
ContextMiddleware.factory
|
||||
[filter:fakeauth]
|
||||
paste.filter_factory = glance.tests.utils:\
|
||||
FakeAuthMiddleware.factory
|
||||
[pipeline:glance-api]
|
||||
pipeline = context rootapp
|
||||
[composite:rootapp]
|
||||
paste.composite_factory = glance.api:root_app_factory
|
||||
/v2: apiv2app
|
||||
[app:apiv2app]
|
||||
paste.app_factory = glance.api.v2.router:API.factory
|
||||
"""))
|
||||
|
||||
def _store_dir(self, store):
|
||||
return os.path.join(self.test_dir, store)
|
||||
|
||||
def setup_stores(self):
|
||||
"""Configures multiple backend stores.
|
||||
|
||||
This configures the API with two file-backed stores (store1
|
||||
and store2) as well as a os_glance_staging_store for imports.
|
||||
|
||||
"""
|
||||
self.config(enabled_backends={'store1': 'file', 'store2': 'file'})
|
||||
glance_store.register_store_opts(CONF,
|
||||
reserved_stores=wsgi.RESERVED_STORES)
|
||||
self.config(default_backend='store1',
|
||||
group='glance_store')
|
||||
self.config(filesystem_store_datadir=self._store_dir('store1'),
|
||||
group='store1')
|
||||
self.config(filesystem_store_datadir=self._store_dir('store2'),
|
||||
group='store2')
|
||||
self.config(filesystem_store_datadir=self._store_dir('staging'),
|
||||
group='os_glance_staging_store')
|
||||
|
||||
glance_store.create_multi_stores(CONF,
|
||||
reserved_stores=wsgi.RESERVED_STORES)
|
||||
glance_store.verify_store()
|
||||
|
||||
def setUp(self):
|
||||
super(SynchronousAPIBase, self).setUp()
|
||||
|
||||
self.setup_database()
|
||||
self.setup_simple_paste()
|
||||
self.setup_stores()
|
||||
|
||||
def start_server(self):
|
||||
"""Builds and "starts" the API server.
|
||||
|
||||
Note that this doesn't actually "start" anything like
|
||||
FunctionalTest does above, but that terminology is used here
|
||||
to make it seem like the same sort of pattern.
|
||||
"""
|
||||
config.set_config_defaults()
|
||||
self.api = config.load_paste_app('glance-api',
|
||||
conf_file=self.paste_config)
|
||||
|
||||
def _headers(self, custom_headers=None):
|
||||
base_headers = {
|
||||
'X-Identity-Status': 'Confirmed',
|
||||
'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
|
||||
'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
|
||||
'X-Tenant-Id': self.TENANT,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Roles': 'admin',
|
||||
}
|
||||
base_headers.update(custom_headers or {})
|
||||
return base_headers
|
||||
|
||||
def api_request(self, method, url, headers=None, data=None,
|
||||
json=None, body_file=None):
|
||||
"""Perform a request against the API.
|
||||
|
||||
NOTE: Most code should use api_get(), api_post(), api_put(),
|
||||
or api_delete() instead!
|
||||
|
||||
:param method: The HTTP method to use (i.e. GET, POST, etc)
|
||||
:param url: The *path* part of the URL to call (i.e. /v2/images)
|
||||
:param headers: Optional updates to the default set of headers
|
||||
:param data: Optional bytes data payload to send (overrides @json)
|
||||
:param json: Optional dict structure to be jsonified and sent as
|
||||
the payload (mutually exclusive with @data)
|
||||
:param body_file: Optional io.IOBase to provide as the input data
|
||||
stream for the request (overrides @data)
|
||||
:returns: A webob.Response object
|
||||
"""
|
||||
headers = self._headers(headers)
|
||||
req = webob.Request.blank(url, method=method,
|
||||
headers=headers)
|
||||
if json and not data:
|
||||
data = jsonutils.dumps(json).encode()
|
||||
if data and not body_file:
|
||||
req.body = data
|
||||
elif body_file:
|
||||
req.body_file = body_file
|
||||
return self.api(req)
|
||||
|
||||
def api_get(self, url, headers=None):
|
||||
"""Perform a GET request against the API.
|
||||
|
||||
:param url: The *path* part of the URL to call (i.e. /v2/images)
|
||||
:param headers: Optional updates to the default set of headers
|
||||
:returns: A webob.Response object
|
||||
"""
|
||||
return self.api_request('GET', url, headers=headers)
|
||||
|
||||
def api_post(self, url, headers=None, data=None, json=None,
|
||||
body_file=None):
|
||||
"""Perform a POST request against the API.
|
||||
|
||||
:param url: The *path* part of the URL to call (i.e. /v2/images)
|
||||
:param headers: Optional updates to the default set of headers
|
||||
:param data: Optional bytes data payload to send (overrides @json)
|
||||
:param json: Optional dict structure to be jsonified and sent as
|
||||
the payload (mutually exclusive with @data)
|
||||
:param body_file: Optional io.IOBase to provide as the input data
|
||||
stream for the request (overrides @data)
|
||||
:returns: A webob.Response object
|
||||
"""
|
||||
return self.api_request('POST', url, headers=headers,
|
||||
data=data, json=json,
|
||||
body_file=body_file)
|
||||
|
||||
def api_put(self, url, headers=None, data=None, json=None, body_file=None):
|
||||
"""Perform a PUT request against the API.
|
||||
|
||||
:param url: The *path* part of the URL to call (i.e. /v2/images)
|
||||
:param headers: Optional updates to the default set of headers
|
||||
:param data: Optional bytes data payload to send (overrides @json,
|
||||
mutually exclusive with body_file)
|
||||
:param json: Optional dict structure to be jsonified and sent as
|
||||
the payload (mutually exclusive with @data)
|
||||
:param body_file: Optional io.IOBase to provide as the input data
|
||||
stream for the request (overrides @data)
|
||||
:returns: A webob.Response object
|
||||
"""
|
||||
return self.api_request('PUT', url, headers=headers,
|
||||
data=data, json=json,
|
||||
body_file=body_file)
|
||||
|
||||
def api_delete(self, url, headers=None):
|
||||
"""Perform a DELETE request against the API.
|
||||
|
||||
:param url: The *path* part of the URL to call (i.e. /v2/images)
|
||||
:param headers: Optional updates to the default set of headers
|
||||
:returns: A webob.Response object
|
||||
"""
|
||||
return self.api_request('DELETE', url, heaers=headers)
|
||||
|
|
|
@ -14,151 +14,19 @@
|
|||
# under the License.
|
||||
|
||||
import datetime
|
||||
import os
|
||||
from testtools import content as ttc
|
||||
import textwrap
|
||||
import time
|
||||
from unittest import mock
|
||||
import uuid
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import fixture as time_fixture
|
||||
from oslo_utils import units
|
||||
import webob
|
||||
|
||||
from glance.common import config
|
||||
from glance.common import wsgi
|
||||
import glance.db.sqlalchemy.api
|
||||
from glance.tests import functional
|
||||
from glance.tests import utils as test_utils
|
||||
import glance_store
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
TENANT1 = str(uuid.uuid4())
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class SynchronousAPIBase(test_utils.BaseTestCase):
|
||||
"""A test base class that provides synchronous calling into the API
|
||||
without starting a separate server, and with a simple paste
|
||||
pipeline. Configured with multi-store and a real database.
|
||||
"""
|
||||
|
||||
@mock.patch('oslo_db.sqlalchemy.enginefacade.writer.get_engine')
|
||||
def setup_database(self, mock_get_engine):
|
||||
db_file = 'sqlite:///%s/test-%s.db' % (self.test_dir,
|
||||
uuid.uuid4())
|
||||
self.config(connection=db_file, group='database')
|
||||
|
||||
# NOTE(danms): Make sure that we clear the current global
|
||||
# database configuration, provision a temporary database file,
|
||||
# and run migrations with our configuration to define the
|
||||
# schema there.
|
||||
glance.db.sqlalchemy.api.clear_db_env()
|
||||
engine = glance.db.sqlalchemy.api.get_engine()
|
||||
mock_get_engine.return_value = engine
|
||||
with mock.patch('logging.config'):
|
||||
# NOTE(danms): The alembic config in the env module will break our
|
||||
# BaseTestCase logging setup. So mock that out to prevent it while
|
||||
# we db_sync.
|
||||
test_utils.db_sync(engine=engine)
|
||||
|
||||
def setup_simple_paste(self):
|
||||
self.paste_config = os.path.join(self.test_dir, 'glance-api-paste.ini')
|
||||
with open(self.paste_config, 'w') as f:
|
||||
f.write(textwrap.dedent("""
|
||||
[filter:context]
|
||||
paste.filter_factory = glance.api.middleware.context:\
|
||||
ContextMiddleware.factory
|
||||
[filter:fakeauth]
|
||||
paste.filter_factory = glance.tests.utils:\
|
||||
FakeAuthMiddleware.factory
|
||||
[pipeline:glance-api]
|
||||
pipeline = context rootapp
|
||||
[composite:rootapp]
|
||||
paste.composite_factory = glance.api:root_app_factory
|
||||
/v2: apiv2app
|
||||
[app:apiv2app]
|
||||
paste.app_factory = glance.api.v2.router:API.factory
|
||||
"""))
|
||||
|
||||
def _store_dir(self, store):
|
||||
return os.path.join(self.test_dir, store)
|
||||
|
||||
def setup_stores(self):
|
||||
self.config(enabled_backends={'store1': 'file', 'store2': 'file'})
|
||||
glance_store.register_store_opts(CONF,
|
||||
reserved_stores=wsgi.RESERVED_STORES)
|
||||
self.config(default_backend='store1',
|
||||
group='glance_store')
|
||||
self.config(filesystem_store_datadir=self._store_dir('store1'),
|
||||
group='store1')
|
||||
self.config(filesystem_store_datadir=self._store_dir('store2'),
|
||||
group='store2')
|
||||
self.config(filesystem_store_datadir=self._store_dir('staging'),
|
||||
group='os_glance_staging_store')
|
||||
|
||||
glance_store.create_multi_stores(CONF,
|
||||
reserved_stores=wsgi.RESERVED_STORES)
|
||||
glance_store.verify_store()
|
||||
|
||||
def setUp(self):
|
||||
super(SynchronousAPIBase, self).setUp()
|
||||
|
||||
self.setup_database()
|
||||
self.setup_simple_paste()
|
||||
self.setup_stores()
|
||||
|
||||
def start_server(self):
|
||||
config.set_config_defaults()
|
||||
self.api = config.load_paste_app('glance-api',
|
||||
conf_file=self.paste_config)
|
||||
|
||||
def _headers(self, custom_headers=None):
|
||||
base_headers = {
|
||||
'X-Identity-Status': 'Confirmed',
|
||||
'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
|
||||
'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
|
||||
'X-Tenant-Id': TENANT1,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Roles': 'admin',
|
||||
}
|
||||
base_headers.update(custom_headers or {})
|
||||
return base_headers
|
||||
|
||||
def api_get(self, url, headers=None):
|
||||
headers = self._headers(headers)
|
||||
req = webob.Request.blank(url, method='GET',
|
||||
headers=headers)
|
||||
return self.api(req)
|
||||
|
||||
def api_post(self, url, data=None, json=None, headers=None):
|
||||
headers = self._headers(headers)
|
||||
req = webob.Request.blank(url, method='POST',
|
||||
headers=headers)
|
||||
if json and not data:
|
||||
data = jsonutils.dumps(json).encode()
|
||||
headers['Content-Type'] = 'application/json'
|
||||
if data:
|
||||
req.body = data
|
||||
LOG.debug(req.as_bytes())
|
||||
return self.api(req)
|
||||
|
||||
def api_put(self, url, data=None, json=None, headers=None, body_file=None):
|
||||
headers = self._headers(headers)
|
||||
req = webob.Request.blank(url, method='PUT',
|
||||
headers=headers)
|
||||
if json and not data:
|
||||
data = jsonutils.dumps(json).encode()
|
||||
if data and not body_file:
|
||||
req.body = data
|
||||
elif body_file:
|
||||
req.body_file = body_file
|
||||
return self.api(req)
|
||||
|
||||
|
||||
class TestImageImportLocking(SynchronousAPIBase):
|
||||
class TestImageImportLocking(functional.SynchronousAPIBase):
|
||||
def _import_copy(self, image_id, stores):
|
||||
"""Do an import of image_id to the given stores."""
|
||||
body = {'method': {'name': 'copy-image'},
|
||||
|
|
Loading…
Reference in New Issue