summaryrefslogtreecommitdiff
path: root/glance/tests/functional/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'glance/tests/functional/__init__.py')
-rw-r--r--glance/tests/functional/__init__.py213
1 files changed, 213 insertions, 0 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)