summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Smith <dansmith@redhat.com>2020-08-10 09:09:43 -0700
committerDan Smith <dansmith@redhat.com>2020-09-03 08:00:35 -0700
commitbeece27e0b2a7009e84e8888d4b6b395567b9458 (patch)
tree67fc5392016a609d22fae8d90ac6d8da702af548
parent7ba0762dd80fd35ae97523b15493be76c79e31bb (diff)
downloadglance-beece27e0b2a7009e84e8888d4b6b395567b9458.tar.gz
Move SynchronousAPIBase to a generalized location
The base class for the tests added in test_images_import_locking provides a mechanism to make API cals directly against the WSGI stack, without starting a separate server and using the local networking. This is useful for cases where fault injection of global state needs to be altered, where this is very difficult in the existing fork-and-exec functional test model. This moves that test base class out to the functional module, expands the documentation a little, and also generalizes the request methods for wider applicability. Change-Id: I59e3b5d5d4b69f076092b9950c0d34467a6636ad (cherry picked from commit 36cbc50e7d70699d6ca5dd4fa81844567e39ddf0)
-rw-r--r--glance/tests/functional/__init__.py213
-rw-r--r--glance/tests/functional/v2/test_images_import_locking.py140
2 files changed, 217 insertions, 136 deletions
diff --git a/glance/tests/functional/__init__.py b/glance/tests/functional/__init__.py
index 8fda3b822..fe2e6fdcb 100644
--- a/glance/tests/functional/__init__.py
+++ b/glance/tests/functional/__init__.py
@@ -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
@@ -1477,3 +1484,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)
diff --git a/glance/tests/functional/v2/test_images_import_locking.py b/glance/tests/functional/v2/test_images_import_locking.py
index d759e47d4..6e9a45f80 100644
--- a/glance/tests/functional/v2/test_images_import_locking.py
+++ b/glance/tests/functional/v2/test_images_import_locking.py
@@ -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'},