HTTP Client refactoring

Library `requests` can handle files, pipes, dictionaries and iterators
as `data` argument.

Use 'json' argument to send json requests.

Rewrite some unittests using mock.

Change-Id: I95b71eb2716dc57708ed105659ffece376bd8344
This commit is contained in:
Sergey Skripnick
2016-10-12 17:15:37 +03:00
committed by Mike Fedosin
parent 7c62625b2a
commit 5e85f0fc3c
12 changed files with 409 additions and 1423 deletions

View File

@@ -26,7 +26,6 @@ import requests
import six import six
from six.moves import urllib from six.moves import urllib
from glareclient._i18n import _
from glareclient.common import exceptions as exc from glareclient.common import exceptions as exc
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -52,43 +51,6 @@ def get_system_ca_file():
LOG.warning("System ca file could not be found.") LOG.warning("System ca file could not be found.")
def _chunk_body(body):
chunk = body
while chunk:
chunk = body.read(CHUNKSIZE)
if not chunk:
break
yield chunk
def _set_request_params(kwargs_params):
"""Handle the common parameters used to send the request."""
data = kwargs_params.pop('data', None)
params = copy.deepcopy(kwargs_params)
headers = params.get('headers', {})
content_type = headers.get('Content-Type')
stream = params.get("stream", False)
if stream:
if data is not None:
data = _chunk_body(data)
content_type = content_type or 'application/octet-stream'
elif data is not None and not isinstance(data, six.string_types):
try:
data = jsonutils.dumps(data)
except TypeError:
raise exc.HTTPBadRequest("json is malformed.")
params['data'] = data
headers.update(
{'Content-Type': content_type or 'application/json'})
params['headers'] = headers
params['stream'] = stream
return params
def _handle_response(resp): def _handle_response(resp):
content_type = resp.headers.get('Content-Type') content_type = resp.headers.get('Content-Type')
if not content_type: if not content_type:
@@ -107,17 +69,6 @@ def _handle_response(resp):
return resp, body_iter return resp, body_iter
def _close_after_stream(response, chunk_size):
"""Iterate over the content and ensure the response is closed after."""
# Yield each chunk in the response body
for chunk in response.iter_content(chunk_size=chunk_size):
yield chunk
# Once we're done streaming the body, ensure everything is closed.
# This will return the connection to the HTTPConnectionPool in urllib3
# and ideally reduce the number of HTTPConnectionPool full warnings.
response.close()
class HTTPClient(object): class HTTPClient(object):
def __init__(self, endpoint, **kwargs): def __init__(self, endpoint, **kwargs):
@@ -305,8 +256,7 @@ class HTTPClient(object):
return creds return creds
def json_request(self, url, method, **kwargs): def json_request(self, url, method, **kwargs):
params = _set_request_params(kwargs) resp = self.request(url, method, **kwargs)
resp = self.request(url, method, **params)
return _handle_response(resp) return _handle_response(resp)
def json_patch_request(self, url, method='PATCH', **kwargs): def json_patch_request(self, url, method='PATCH', **kwargs):
@@ -336,37 +286,14 @@ class SessionClient(adapter.LegacyJsonAdapter):
"""HTTP client based on Keystone client session.""" """HTTP client based on Keystone client session."""
def request(self, url, method, **kwargs): def request(self, url, method, **kwargs):
params = _set_request_params(kwargs)
redirect = kwargs.get('redirect')
resp, body = super(SessionClient, self).request( resp, body = super(SessionClient, self).request(
url, method, url, method, **kwargs)
**params)
if 400 <= resp.status_code < 600: if resp.status_code == 300 or (400 <= resp.status_code < 600):
raise exc.from_response(resp)
elif resp.status_code in (301, 302, 305):
if redirect:
location = resp.headers.get('location')
path = self.strip_endpoint(location)
resp = self.request(path, method, **kwargs)
elif resp.status_code == 300:
raise exc.from_response(resp) raise exc.from_response(resp)
if resp.headers.get('Content-Type') == 'application/octet-stream':
body = _close_after_stream(resp, CHUNKSIZE)
return resp, body return resp, body
def strip_endpoint(self, location):
if location is None:
message = _("Location not returned with 302")
raise exc.InvalidEndpoint(message=message)
if (self.endpoint_override is not None and
location.lower().startswith(self.endpoint_override.lower())):
return location[len(self.endpoint_override):]
else:
return location
def construct_http_client(*args, **kwargs): def construct_http_client(*args, **kwargs):
session = kwargs.pop('session', None) session = kwargs.pop('session', None)

View File

@@ -13,9 +13,12 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import decimal
import os
import sys import sys
import six SPIN_CHARS = ('-', '\\', '|', '/')
CHUNKSIZE = 1024 * 64 # 64kB
class _ProgressBarBase(object): class _ProgressBarBase(object):
@@ -30,21 +33,37 @@ class _ProgressBarBase(object):
:note: The progress will be displayed only if sys.stdout is a tty. :note: The progress will be displayed only if sys.stdout is a tty.
""" """
def __init__(self, wrapped, totalsize): def __init__(self, wrapped):
self._wrapped = wrapped self._wrapped = wrapped
self._totalsize = float(totalsize)
self._show_progress = sys.stdout.isatty() self._show_progress = sys.stdout.isatty()
self._percent = 0 self._percent = 0
self._totalread = 0
self._spin_index = 0
if hasattr(wrapped, "len"):
self._totalsize = wrapped.len
elif hasattr(wrapped, "fileno"):
self._totalsize = os.fstat(wrapped.fileno()).st_size
else:
self._totalsize = 0
def _display_progress_bar(self, size_read): def _display_progress_bar(self, size_read):
self._totalread += size_read
if self._show_progress: if self._show_progress:
if self._totalsize == 0: if self._totalsize:
self._totalsize = size_read percent = float(self._totalread) / float(self._totalsize)
self._percent += size_read / self._totalsize # Output something like this: [==========> ] 49%
# Output something like this: [==========> ] 49% sys.stdout.write('\r[{0:<30}] {1:.0%}'.format(
sys.stdout.write('\r[{0:<30}] {1:.0%}'.format( '=' * int(decimal.Decimal(percent * 29).quantize(
'=' * int(round(self._percent * 29)) + '>', self._percent decimal.Decimal('1'),
)) rounding=decimal.ROUND_HALF_UP)) + '>', percent
))
else:
sys.stdout.write(
'\r[%s] %d bytes' % (SPIN_CHARS[self._spin_index],
self._totalread))
self._spin_index += 1
if self._spin_index == len(SPIN_CHARS):
self._spin_index = 0
sys.stdout.flush() sys.stdout.flush()
def __getattr__(self, attr): def __getattr__(self, attr):
@@ -70,24 +89,12 @@ class VerboseFileWrapper(_ProgressBarBase):
sys.stdout.write('\n') sys.stdout.write('\n')
return data return data
class VerboseIteratorWrapper(_ProgressBarBase):
"""An iterator wrapper with a progress bar.
The iterator wrapper shows and advances a progress bar whenever the
wrapped data is consumed from the iterator.
:note: Use only with iterator that yield strings.
"""
def __iter__(self): def __iter__(self):
return self return self
def next(self): def next(self):
try: try:
data = six.next(self._wrapped) data = self._wrapped.next()
# NOTE(mouad): Assuming that data is a string b/c otherwise calling
# len function will not make any sense.
self._display_progress_bar(len(data)) self._display_progress_bar(len(data))
return data return data
except StopIteration: except StopIteration:

View File

@@ -18,7 +18,6 @@ from __future__ import print_function
import errno import errno
import hashlib import hashlib
import os import os
import six
import sys import sys
if os.name == 'nt': if os.name == 'nt':
@@ -28,6 +27,7 @@ else:
from oslo_utils import encodeutils from oslo_utils import encodeutils
from oslo_utils import importutils from oslo_utils import importutils
import requests
SENSITIVE_HEADERS = ('X-Auth-Token', ) SENSITIVE_HEADERS = ('X-Auth-Token', )
@@ -58,41 +58,40 @@ def exit(msg='', exit_code=1):
sys.exit(exit_code) sys.exit(exit_code)
def integrity_iter(iter, checksum): class ResponseBlobWrapper(object):
"""Check blob integrity. """Represent HTTP response as iterator with known length."""
:raises: IOError def __init__(self, resp, verify_md5=True):
""" self.hash_md5 = resp.headers.get("Content-MD5")
md5sum = hashlib.md5() self.check_md5 = hashlib.md5()
for chunk in iter: if 301 <= resp.status_code <= 302:
yield chunk # NOTE(sskripnick): handle redirect manually to prevent sending
if isinstance(chunk, six.string_types): # auth token to external resource.
chunk = six.b(chunk) # Use stream=True to prevent reading whole response into memory.
md5sum.update(chunk) # Set Accept-Encoding explicitly to "identity" because setting
md5sum = md5sum.hexdigest() # stream=True forces Accept-Encoding to be "gzip, defalate".
if md5sum != checksum: # It should be "identity" because we should know Content-Length.
raise IOError(errno.EPIPE, resp = requests.get(resp.headers.get("Location"),
'Corrupt blob download. Checksum was %s expected %s' % headers={"Accept-Encoding": "identity"})
(md5sum, checksum)) self.len = resp.headers.get("Content-Length", 0)
self.iter = resp.iter_content(65536)
class IterableWithLength(object):
def __init__(self, iterable, length):
self.iterable = iterable
self.length = length
def __iter__(self): def __iter__(self):
try: return self
for chunk in self.iterable:
yield chunk
finally:
self.iterable.close()
def next(self): def next(self):
return next(self.iterable) try:
data = self.iter.next()
self.check_md5.update(data)
return data
except StopIteration:
if self.check_md5.hexdigest() != self.hash_md5:
raise IOError(errno.EPIPE,
'Checksum mismatch: %s (expected %s)' %
(self.check_md5.hexdigest(), self.hash_md5))
raise
def __len__(self): __next__ = next
return self.length
def get_item_properties(item, fields, mixed_case_fields=None, formatters=None): def get_item_properties(item, fields, mixed_case_fields=None, formatters=None):
@@ -158,60 +157,3 @@ def save_blob(data, path):
finally: finally:
if path is not None: if path is not None:
blob.close() blob.close()
def get_data_file(blob):
if blob:
return open(blob, 'rb')
else:
# distinguish cases where:
# (1) stdin is not valid (as in cron jobs):
# glare ... <&-
# (2) blob is provided through standard input:
# glare ... < /tmp/file or cat /tmp/file | glare ...
# (3) no blob provided:
# glare ...
try:
os.fstat(0)
except OSError:
# (1) stdin is not valid (closed...)
return None
if not sys.stdin.isatty():
# (2) blob data is provided through standard input
blob_data = sys.stdin
if hasattr(sys.stdin, 'buffer'):
blob_data = sys.stdin.buffer
if msvcrt:
msvcrt.setmode(blob_data.fileno(), os.O_BINARY)
return blob_data
else:
# (3) no blob data provided
return None
def get_file_size(file_obj):
"""Analyze file-like object and attempt to determine its size.
:param file_obj: file-like object.
:retval The file's size or None if it cannot be determined.
"""
if (hasattr(file_obj, 'seek') and hasattr(file_obj, 'tell') and
(six.PY2 or six.PY3 and file_obj.seekable())):
try:
curr = file_obj.tell()
file_obj.seek(0, os.SEEK_END)
size = file_obj.tell()
file_obj.seek(curr)
return size
except IOError as e:
if e.errno == errno.ESPIPE:
# Illegal seek. This means the file object
# is a pipe (e.g. the user is trying
# to pipe blob to the client,
# echo testdata | bin/glare add blah...), or
# that file object is empty, or that a file-like
# object which doesn't support 'seek/tell' has
# been supplied.
return
else:
raise

View File

@@ -19,6 +19,7 @@ from osc_lib.command import command
from glareclient.common import progressbar from glareclient.common import progressbar
from glareclient.common import utils from glareclient.common import utils
from glareclient import exc
from glareclient.osc.v1 import TypeMapperAction from glareclient.osc.v1 import TypeMapperAction
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -107,11 +108,15 @@ class UploadBlob(command.ShowOne):
parsed_args.blob_property = _default_blob_property( parsed_args.blob_property = _default_blob_property(
parsed_args.type_name) parsed_args.type_name)
blob = utils.get_data_file(parsed_args.file) if parsed_args.file is None:
if sys.stdin.isatty():
raise exc.CommandError('Blob file should be specified or '
'explicitly connected to stdin')
blob = sys.stdin
else:
blob = open(parsed_args.file, 'rb')
if parsed_args.progress: if parsed_args.progress:
file_size = utils.get_file_size(blob) blob = progressbar.VerboseFileWrapper(blob)
if file_size is not None:
blob = progressbar.VerboseFileWrapper(blob, file_size)
client.artifacts.upload_blob(af_id, parsed_args.blob_property, blob, client.artifacts.upload_blob(af_id, parsed_args.blob_property, blob,
content_type=parsed_args.content_type, content_type=parsed_args.content_type,
@@ -186,7 +191,7 @@ class DownloadBlob(command.Command):
parsed_args.blob_property, parsed_args.blob_property,
type_name=parsed_args.type_name) type_name=parsed_args.type_name)
if parsed_args.progress: if parsed_args.progress:
data = progressbar.VerboseIteratorWrapper(data, len(data)) data = progressbar.VerboseFileWrapper(data)
if not (sys.stdout.isatty() and parsed_args.file is None): if not (sys.stdout.isatty() and parsed_args.file is None):
utils.save_blob(data, parsed_args.file) utils.save_blob(data, parsed_args.file)
else: else:

View File

@@ -14,12 +14,108 @@
import mock import mock
from glareclient import exc
from glareclient.osc.v1 import blobs as osc_blob from glareclient.osc.v1 import blobs as osc_blob
from glareclient.tests.unit.osc.v1 import fakes from glareclient.tests.unit.osc.v1 import fakes
from glareclient.v1 import artifacts as api_art from glareclient.v1 import artifacts as api_art
import testtools import testtools
class TestUpload(testtools.TestCase):
def setUp(self):
self.mock_app = mock.Mock()
self.mock_args = mock.Mock()
self.mock_manager = mock.Mock()
self.mock_manager.artifacts.get.return_value = {'image': {}}
super(TestUpload, self).setUp()
@mock.patch('glareclient.osc.v1.blobs.progressbar')
@mock.patch('glareclient.osc.v1.blobs.sys')
@mock.patch('glareclient.osc.v1.blobs.open', create=True)
@mock.patch('glareclient.osc.v1.blobs.get_artifact_id')
def test_upload_file_progress(self, mock_get_id,
mock_open, mock_sys, mock_progressbar):
mock_parsed_args = mock.Mock(name='test-id',
id=True,
blob_property='image',
file='/path/file',
progress=True,
content_type='application/test',
type_name='test-type')
mock_get_id.return_value = 'test-id'
cli = osc_blob.UploadBlob(self.mock_app, self.mock_args)
cli.app.client_manager.artifact = self.mock_manager
cli.dict2columns = mock.Mock(return_value=42)
self.assertEqual(42, cli.take_action(mock_parsed_args))
cli.dict2columns.assert_called_once_with({'blob_property': 'image'})
upload_args = ['test-id', 'image',
mock_progressbar.VerboseFileWrapper.return_value]
upload_kwargs = {'content_type': 'application/test',
'type_name': 'test-type'}
self.mock_manager.artifacts.upload_blob.\
assert_called_once_with(*upload_args, **upload_kwargs)
@mock.patch('glareclient.osc.v1.blobs.sys')
@mock.patch('glareclient.osc.v1.blobs.open', create=True)
@mock.patch('glareclient.osc.v1.blobs.get_artifact_id')
def test_upload_file_no_progress(self, mock_get_id, mock_open, mock_sys):
mock_parsed_args = mock.Mock(name='test-id',
id=True,
blob_property='image',
progress=False,
file='/path/file',
content_type='application/test',
type_name='test-type')
mock_get_id.return_value = 'test-id'
cli = osc_blob.UploadBlob(self.mock_app, self.mock_args)
cli.app.client_manager.artifact = self.mock_manager
cli.dict2columns = mock.Mock(return_value=42)
self.assertEqual(42, cli.take_action(mock_parsed_args))
cli.dict2columns.assert_called_once_with({'blob_property': 'image'})
upload_args = ['test-id', 'image', mock_open.return_value]
upload_kwargs = {'content_type': 'application/test',
'type_name': 'test-type'}
self.mock_manager.artifacts.upload_blob.\
assert_called_once_with(*upload_args, **upload_kwargs)
@mock.patch('glareclient.osc.v1.blobs.sys')
@mock.patch('glareclient.osc.v1.blobs.get_artifact_id')
def test_upload_file_stdin(self, mock_get_id, mock_sys):
mock_sys.stdin.isatty.return_value = False
mock_parsed_args = mock.Mock(name='test-id',
id=True,
blob_property='image',
progress=False,
file=None,
content_type='application/test',
type_name='test-type')
mock_get_id.return_value = 'test-id'
cli = osc_blob.UploadBlob(self.mock_app, self.mock_args)
cli.app.client_manager.artifact = self.mock_manager
cli.dict2columns = mock.Mock(return_value=42)
self.assertEqual(42, cli.take_action(mock_parsed_args))
cli.dict2columns.assert_called_once_with({'blob_property': 'image'})
upload_args = ['test-id', 'image', mock_sys.stdin]
upload_kwargs = {'content_type': 'application/test',
'type_name': 'test-type'}
self.mock_manager.artifacts.upload_blob.\
assert_called_once_with(*upload_args, **upload_kwargs)
@mock.patch('glareclient.osc.v1.blobs.sys')
def test_upload_file_stdin_isatty(self, mock_sys):
mock_sys.stdin.isatty.return_value = True
mock_parsed_args = mock.Mock(id='test-id',
blob_property='image',
progress=False,
file=None,
content_type='application/test',
type_name='test-type')
cli = osc_blob.UploadBlob(self.mock_app, self.mock_args)
cli.app.client_manager.artifact = self.mock_manager
self.assertRaises(exc.CommandError, cli.take_action, mock_parsed_args)
class TestBlobs(fakes.TestArtifacts): class TestBlobs(fakes.TestArtifacts):
def setUp(self): def setUp(self):
super(TestBlobs, self).setUp() super(TestBlobs, self).setUp()
@@ -28,187 +124,6 @@ class TestBlobs(fakes.TestArtifacts):
self.http = mock.MagicMock() self.http = mock.MagicMock()
class TestUploadBlob(TestBlobs):
def setUp(self):
super(TestUploadBlob, self).setUp()
self.blob_mock.call.return_value = \
api_art.Controller(self.http, type_name='images')
# Command to test
self.cmd = osc_blob.UploadBlob(self.app, None)
self.COLUMNS = ('blob_property', 'content_type', 'external',
'md5', 'sha1', 'sha256', 'size', 'status', 'url')
def test_upload_images(self):
exp_data = ('image', 'application/octet-stream', False,
'35d83e8eedfbdb87ff97d1f2761f8ebf',
'942854360eeec1335537702399c5aed940401602',
'd8a7834fc6652f316322d80196f6dcf2'
'94417030e37c15412e4deb7a67a367dd',
594, 'active', 'fake_url')
arglist = ['images',
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
'--file', '/path/to/file']
verify = [('type_name', 'images')]
parsed_args = self.check_parser(self.cmd, arglist, verify)
columns, data = self.cmd.take_action(parsed_args)
self.assertEqual(self.COLUMNS, columns)
self.assertEqual(exp_data, data)
def test_upload_tosca_template(self):
exp_data = ('template', 'application/octet-stream', False,
'35d83e8eedfbdb87ff97d1f2761f8ebf',
'942854360eeec1335537702399c5aed940401602',
'd8a7834fc6652f316322d80196f6dcf2'
'94417030e37c15412e4deb7a67a367dd',
594, 'active', 'fake_url')
arglist = ['tosca_templates',
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
'--file', '/path/to/file']
verify = [('type_name', 'tosca_templates')]
parsed_args = self.check_parser(self.cmd, arglist, verify)
columns, data = self.cmd.take_action(parsed_args)
self.assertEqual(self.COLUMNS, columns)
self.assertEqual(exp_data, data)
def test_upload_heat_template(self):
exp_data = ('template', 'application/octet-stream', False,
'35d83e8eedfbdb87ff97d1f2761f8ebf',
'942854360eeec1335537702399c5aed940401602',
'd8a7834fc6652f316322d80196f6dcf2'
'94417030e37c15412e4deb7a67a367dd',
594, 'active', 'fake_url')
arglist = ['heat_templates',
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
'--file', '/path/to/file']
verify = [('type_name', 'heat_templates')]
parsed_args = self.check_parser(self.cmd, arglist, verify)
columns, data = self.cmd.take_action(parsed_args)
self.assertEqual(self.COLUMNS, columns)
self.assertEqual(exp_data, data)
def test_upload_environment(self):
exp_data = ('environment', 'application/octet-stream', False,
'35d83e8eedfbdb87ff97d1f2761f8ebf',
'942854360eeec1335537702399c5aed940401602',
'd8a7834fc6652f316322d80196f6dcf2'
'94417030e37c15412e4deb7a67a367dd',
594, 'active', 'fake_url')
arglist = ['heat_environments',
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
'--file', '/path/to/file']
verify = [('type_name', 'heat_environments')]
parsed_args = self.check_parser(self.cmd, arglist, verify)
columns, data = self.cmd.take_action(parsed_args)
self.assertEqual(self.COLUMNS, columns)
self.assertEqual(exp_data, data)
def test_upload_package(self):
exp_data = ('package', 'application/octet-stream', False,
'35d83e8eedfbdb87ff97d1f2761f8ebf',
'942854360eeec1335537702399c5aed940401602',
'd8a7834fc6652f316322d80196f6dcf2'
'94417030e37c15412e4deb7a67a367dd',
594, 'active', 'fake_url')
arglist = ['murano_packages',
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
'--file', '/path/to/file']
verify = [('type_name', 'murano_packages')]
parsed_args = self.check_parser(self.cmd, arglist, verify)
columns, data = self.cmd.take_action(parsed_args)
self.assertEqual(self.COLUMNS, columns)
self.assertEqual(exp_data, data)
def test_upload_bad(self):
arglist = ['user_type',
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
'--file', '/path/to/file']
verify = [('type_name', 'user_type')]
parsed_args = self.check_parser(self.cmd, arglist, verify)
with testtools.ExpectedException(SystemExit):
self.cmd.take_action(parsed_args)
def test_upload_with_custom_content_type(self):
exp_data = ('template', 'application/x-yaml', False,
'35d83e8eedfbdb87ff97d1f2761f8ebf',
'942854360eeec1335537702399c5aed940401602',
'd8a7834fc6652f316322d80196f6dcf2'
'94417030e37c15412e4deb7a67a367dd',
594, 'active', 'fake_url')
mocked_get = {
"status": "active",
"url": "fake_url",
"md5": "35d83e8eedfbdb87ff97d1f2761f8ebf",
"sha1": "942854360eeec1335537702399c5aed940401602",
"sha256": "d8a7834fc6652f316322d80196f6dcf2"
"94417030e37c15412e4deb7a67a367dd",
"external": False,
"content_type": "application/x-yaml",
"size": 594}
self.app.client_manager.artifact.artifacts.get = \
lambda id, type_name: {'template': mocked_get}
arglist = ['tosca_templates',
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
'--file', '/path/to/file',
'--content-type', 'application/x-yaml']
verify = [('type_name', 'tosca_templates')]
parsed_args = self.check_parser(self.cmd, arglist, verify)
columns, data = self.cmd.take_action(parsed_args)
self.assertEqual(self.COLUMNS, columns)
self.assertEqual(exp_data, data)
def test_upload_blob_with_blob_prop(self):
exp_data = ('blob', 'application/octet-stream', False,
'35d83e8eedfbdb87ff97d1f2761f8ebf',
'942854360eeec1335537702399c5aed940401602',
'd8a7834fc6652f316322d80196f6dcf2'
'94417030e37c15412e4deb7a67a367dd',
594, 'active', 'fake_url')
arglist = ['images',
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
'--file', '/path/to/file',
'--blob-property', 'blob']
verify = [('type_name', 'images')]
parsed_args = self.check_parser(self.cmd, arglist, verify)
columns, data = self.cmd.take_action(parsed_args)
self.assertEqual(self.COLUMNS, columns)
self.assertEqual(exp_data, data)
def test_upload_blob_dict(self):
exp_data = ('nested_templates/blob', 'application/octet-stream',
False,
'35d83e8eedfbdb87ff97d1f2761f8ebf',
'942854360eeec1335537702399c5aed940401602',
'd8a7834fc6652f316322d80196f6dcf2'
'94417030e37c15412e4deb7a67a367dd',
594, 'active', 'fake_url')
arglist = ['images',
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
'--file', '/path/to/file',
'--blob-property', 'nested_templates/blob']
verify = [('type_name', 'images')]
parsed_args = self.check_parser(self.cmd, arglist, verify)
self.app.client_manager.artifact.artifacts.get = \
lambda *args, **kwargs: {
'nested_templates': {'blob': fakes.blob_fixture}
}
columns, data = self.cmd.take_action(parsed_args)
self.app.client_manager.artifact.artifacts.get = fakes.mock_get
self.assertEqual(self.COLUMNS, columns)
self.assertEqual(exp_data, data)
class TestDownloadBlob(TestBlobs): class TestDownloadBlob(TestBlobs):
def setUp(self): def setUp(self):
super(TestDownloadBlob, self).setUp() super(TestDownloadBlob, self).setUp()

View File

@@ -119,10 +119,7 @@ class HttpClientTest(testtools.TestCase):
mock_request.assert_called_once_with( mock_request.assert_called_once_with(
'GET', 'http://example.com:9494', 'GET', 'http://example.com:9494',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'})
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_http_json_request_argument_passed_to_requests(self, mock_request): def test_http_json_request_argument_passed_to_requests(self, mock_request):
"""Check that we have sent the proper arguments to requests.""" """Check that we have sent the proper arguments to requests."""
@@ -148,9 +145,7 @@ class HttpClientTest(testtools.TestCase):
cert=('RANDOM_CERT_FILE', 'RANDOM_KEY_FILE'), cert=('RANDOM_CERT_FILE', 'RANDOM_KEY_FILE'),
verify=True, verify=True,
data='text', data='text',
stream=False, headers={'X-Auth-Url': 'http://AUTH_URL',
headers={'Content-Type': 'application/json',
'X-Auth-Url': 'http://AUTH_URL',
'User-Agent': 'python-glareclient'}) 'User-Agent': 'python-glareclient'})
def test_http_json_request_w_req_body(self, mock_request): def test_http_json_request_w_req_body(self, mock_request):
@@ -169,9 +164,7 @@ class HttpClientTest(testtools.TestCase):
'GET', 'http://example.com:9494', 'GET', 'http://example.com:9494',
data='test-body', data='test-body',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'})
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_http_json_request_non_json_resp_cont_type(self, mock_request): def test_http_json_request_non_json_resp_cont_type(self, mock_request):
# Record a 200 # Record a 200
@@ -187,9 +180,7 @@ class HttpClientTest(testtools.TestCase):
mock_request.assert_called_once_with( mock_request.assert_called_once_with(
'GET', 'http://example.com:9494', data='test-data', 'GET', 'http://example.com:9494', data='test-data',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'})
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_http_json_request_invalid_json(self, mock_request): def test_http_json_request_invalid_json(self, mock_request):
# Record a 200 # Record a 200
@@ -206,10 +197,7 @@ class HttpClientTest(testtools.TestCase):
mock_request.assert_called_once_with( mock_request.assert_called_once_with(
'GET', 'http://example.com:9494', 'GET', 'http://example.com:9494',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'})
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_http_manual_redirect_delete(self, mock_request): def test_http_manual_redirect_delete(self, mock_request):
mock_request.side_effect = [ mock_request.side_effect = [
@@ -229,16 +217,10 @@ class HttpClientTest(testtools.TestCase):
mock_request.assert_has_calls([ mock_request.assert_has_calls([
mock.call('DELETE', 'http://example.com:9494/foo', mock.call('DELETE', 'http://example.com:9494/foo',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'}),
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'}),
mock.call('DELETE', 'http://example.com:9494/foo/bar', mock.call('DELETE', 'http://example.com:9494/foo/bar',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'})
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
]) ])
def test_http_manual_redirect_post(self, mock_request): def test_http_manual_redirect_post(self, mock_request):
@@ -253,22 +235,18 @@ class HttpClientTest(testtools.TestCase):
'{}')] '{}')]
client = http.HTTPClient('http://example.com:9494/foo') client = http.HTTPClient('http://example.com:9494/foo')
resp, body = client.json_request('', 'POST') resp, body = client.json_request('', 'POST', json={})
self.assertEqual(200, resp.status_code) self.assertEqual(200, resp.status_code)
mock_request.assert_has_calls([ mock_request.assert_has_calls([
mock.call('POST', 'http://example.com:9494/foo', mock.call('POST', 'http://example.com:9494/foo',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'},
data=None, json={}),
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'}),
mock.call('POST', 'http://example.com:9494/foo/bar', mock.call('POST', 'http://example.com:9494/foo/bar',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'},
data=None, json={})
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
]) ])
def test_http_manual_redirect_put(self, mock_request): def test_http_manual_redirect_put(self, mock_request):
@@ -283,22 +261,18 @@ class HttpClientTest(testtools.TestCase):
'{}')] '{}')]
client = http.HTTPClient('http://example.com:9494/foo') client = http.HTTPClient('http://example.com:9494/foo')
resp, body = client.json_request('', 'PUT') resp, body = client.json_request('', 'PUT', json={})
self.assertEqual(200, resp.status_code) self.assertEqual(200, resp.status_code)
mock_request.assert_has_calls([ mock_request.assert_has_calls([
mock.call('PUT', 'http://example.com:9494/foo', mock.call('PUT', 'http://example.com:9494/foo',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'},
data=None, json={}),
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'}),
mock.call('PUT', 'http://example.com:9494/foo/bar', mock.call('PUT', 'http://example.com:9494/foo/bar',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'},
data=None, json={})
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
]) ])
def test_http_manual_redirect_prohibited(self, mock_request): def test_http_manual_redirect_prohibited(self, mock_request):
@@ -313,10 +287,7 @@ class HttpClientTest(testtools.TestCase):
mock_request.assert_called_once_with( mock_request.assert_called_once_with(
'DELETE', 'http://example.com:9494/foo', 'DELETE', 'http://example.com:9494/foo',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'})
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_http_manual_redirect_error_without_location(self, mock_request): def test_http_manual_redirect_error_without_location(self, mock_request):
mock_request.return_value = \ mock_request.return_value = \
@@ -330,10 +301,7 @@ class HttpClientTest(testtools.TestCase):
mock_request.assert_called_once_with( mock_request.assert_called_once_with(
'DELETE', 'http://example.com:9494/foo', 'DELETE', 'http://example.com:9494/foo',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'})
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_http_json_request_redirect(self, mock_request): def test_http_json_request_redirect(self, mock_request):
# Record the 302 # Record the 302
@@ -344,7 +312,7 @@ class HttpClientTest(testtools.TestCase):
''), ''),
fakes.FakeHTTPResponse( fakes.FakeHTTPResponse(
200, 'OK', 200, 'OK',
{'content-type': 'application/json'}, {},
'{}')] '{}')]
client = http.HTTPClient('http://example.com:9494') client = http.HTTPClient('http://example.com:9494')
@@ -355,16 +323,10 @@ class HttpClientTest(testtools.TestCase):
mock_request.assert_has_calls([ mock_request.assert_has_calls([
mock.call('GET', 'http://example.com:9494', mock.call('GET', 'http://example.com:9494',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'}),
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'}),
mock.call('GET', 'http://example.com:9494', mock.call('GET', 'http://example.com:9494',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'})
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
]) ])
def test_http_404_json_request(self, mock_request): def test_http_404_json_request(self, mock_request):
@@ -381,10 +343,7 @@ class HttpClientTest(testtools.TestCase):
mock_request.assert_called_once_with( mock_request.assert_called_once_with(
'GET', 'http://example.com:9494', 'GET', 'http://example.com:9494',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'})
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_http_300_json_request(self, mock_request): def test_http_300_json_request(self, mock_request):
mock_request.return_value = \ mock_request.return_value = \
@@ -401,10 +360,7 @@ class HttpClientTest(testtools.TestCase):
mock_request.assert_called_once_with( mock_request.assert_called_once_with(
'GET', 'http://example.com:9494', 'GET', 'http://example.com:9494',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'})
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_fake_json_request(self, mock_request): def test_fake_json_request(self, mock_request):
headers = {'User-Agent': 'python-glareclient'} headers = {'User-Agent': 'python-glareclient'}
@@ -453,10 +409,7 @@ class HttpClientTest(testtools.TestCase):
mock_request.assert_called_once_with( mock_request.assert_called_once_with(
'GET', 'http://example.com:9494', 'GET', 'http://example.com:9494',
allow_redirects=False, allow_redirects=False,
stream=False, headers={'User-Agent': 'python-glareclient'},
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'},
timeout=float(123)) timeout=float(123))
def test_get_system_ca_file(self, mock_request): def test_get_system_ca_file(self, mock_request):

View File

@@ -13,63 +13,65 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import sys import mock
from six import StringIO
import six
import testtools import testtools
from glareclient.common import progressbar from glareclient.common import progressbar
from glareclient.tests import utils as test_utils
MOD = 'glareclient.common.progressbar'
class TestProgressBarWrapper(testtools.TestCase): class TestProgressBar(testtools.TestCase):
def test_iter_iterator_display_progress_bar(self): @mock.patch(MOD + '.os')
size = 100 def test_totalsize_fileno(self, mock_os):
iterator = iter('X' * 100) mock_os.fstat.return_value.st_size = 43
saved_stdout = sys.stdout fake_file = mock.Mock()
try: del fake_file.len
sys.stdout = output = test_utils.FakeTTYStdout() fake_file.fileno.return_value = 42
# Consume iterator. pb = progressbar.VerboseFileWrapper(fake_file)
data = list(progressbar.VerboseIteratorWrapper(iterator, size)) self.assertEqual(43, pb._totalsize)
self.assertEqual(['X'] * 100, data) mock_os.fstat.assert_called_once_with(42)
self.assertEqual(
'[%s>] 100%%\n' % ('=' * 29),
output.getvalue()
)
finally:
sys.stdout = saved_stdout
def test_iter_file_display_progress_bar(self): @mock.patch(MOD + '.sys')
size = 98304 def test__display_progress_bar(self, mock_sys):
file_obj = six.StringIO('X' * size) fake_file = StringIO('test') # 4 bytes
saved_stdout = sys.stdout fake_file.len = 4
try: pb = progressbar.VerboseFileWrapper(fake_file)
sys.stdout = output = test_utils.FakeTTYStdout() pb._display_progress_bar(2) # 2 of 4 bytes = 50%
file_obj = progressbar.VerboseFileWrapper(file_obj, size) pb._display_progress_bar(1) # 3 of 4 bytes = 75%
chunksize = 1024 pb._display_progress_bar(1) # 4 of 4 bytes = 100%
chunk = file_obj.read(chunksize) expected = [
while chunk: mock.call('\r[===============> ] 50%'),
chunk = file_obj.read(chunksize) mock.call('\r[======================> ] 75%'),
self.assertEqual( mock.call('\r[=============================>] 100%'),
'[%s>] 100%%\n' % ('=' * 29), ]
output.getvalue() self.assertEqual(expected, mock_sys.stdout.write.mock_calls)
)
finally:
sys.stdout = saved_stdout
def test_iter_file_no_tty(self): @mock.patch(MOD + '.sys')
size = 98304 def test__display_progress_bar_unknown_len(self, mock_sys):
file_obj = six.StringIO('X' * size) fake_file = StringIO('')
saved_stdout = sys.stdout fake_file.len = 0
try: pb = progressbar.VerboseFileWrapper(fake_file)
sys.stdout = output = test_utils.FakeNoTTYStdout() for i in range(6):
file_obj = progressbar.VerboseFileWrapper(file_obj, size) pb._display_progress_bar(1)
chunksize = 1024 expected = [
chunk = file_obj.read(chunksize) mock.call('\r[-] 1 bytes'),
while chunk: mock.call('\r[\\] 2 bytes'),
chunk = file_obj.read(chunksize) mock.call('\r[|] 3 bytes'),
# If stdout is not a tty progress bar should do nothing. mock.call('\r[/] 4 bytes'),
self.assertEqual('', output.getvalue()) mock.call('\r[-] 5 bytes'),
finally: mock.call('\r[\\] 6 bytes'),
sys.stdout = saved_stdout ]
self.assertEqual(expected, mock_sys.stdout.write.mock_calls)
@mock.patch(MOD + '._ProgressBarBase.__init__')
@mock.patch(MOD + '._ProgressBarBase._display_progress_bar')
def test_read(self, mock_display_progress_bar, mock_init):
mock_init.return_value = None
pb = progressbar.VerboseFileWrapper()
pb._wrapped = mock.Mock(len=42)
pb._wrapped.read.return_value = 'ok'
pb.read(2)
mock_display_progress_bar.assert_called_once_with(2)

View File

@@ -14,8 +14,6 @@
# under the License. # under the License.
import mock
import six
import testtools import testtools
from glareclient.common import utils from glareclient.common import utils
@@ -30,36 +28,3 @@ class TestUtils(testtools.TestCase):
self.assertEqual("1.4GB", utils.make_size_human_readable(1476395008)) self.assertEqual("1.4GB", utils.make_size_human_readable(1476395008))
self.assertEqual("9.3MB", utils.make_size_human_readable(9761280)) self.assertEqual("9.3MB", utils.make_size_human_readable(9761280))
self.assertEqual("0B", utils.make_size_human_readable(None)) self.assertEqual("0B", utils.make_size_human_readable(None))
def test_get_new_file_size(self):
size = 98304
file_obj = six.StringIO('X' * size)
try:
self.assertEqual(size, utils.get_file_size(file_obj))
# Check that get_file_size didn't change original file position.
self.assertEqual(0, file_obj.tell())
finally:
file_obj.close()
def test_get_consumed_file_size(self):
size, consumed = 98304, 304
file_obj = six.StringIO('X' * size)
file_obj.seek(consumed)
try:
self.assertEqual(size, utils.get_file_size(file_obj))
# Check that get_file_size didn't change original file position.
self.assertEqual(consumed, file_obj.tell())
finally:
file_obj.close()
def test_iterable_closes(self):
# Regression test for bug 1461678.
def _iterate(i):
for chunk in i:
raise(IOError)
data = six.moves.StringIO('somestring')
data.close = mock.Mock()
i = utils.IterableWithLength(data, 10)
self.assertRaises(IOError, _iterate, i)
data.close.assert_called_with()

View File

@@ -1,261 +0,0 @@
# Copyright 2016 OpenStack Foundation
# All 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.
data_fixtures = {
'/artifacts/images?limit=20': {
'GET': (
# headers
{},
# response
{'images': [
{
'name': 'art1',
'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
'version': '0.0.0'
},
{
'name': 'art2',
'id': 'db721fb0-5b85-4738-9401-f161d541de5e',
'version': '0.0.0'
},
{
'name': 'art3',
'id': 'e4f027d2-bff3-4084-a2ba-f31cb5e3067f',
'version': '0.0.0'
},
]},
),
},
'/artifacts/images?page_size=2': {
'GET': (
{},
{'images': [
{
'name': 'art1',
'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
'version': '0.0.0'
},
{
'name': 'art2',
'id': 'db721fb0-5b85-4738-9401-f161d541de5e',
'version': '0.0.0'
},
]},
),
},
'/artifacts/images?limit=2': {
'GET': (
{},
{'images': [
{
'name': 'art1',
'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
'version': '0.0.0'
},
{
'name': 'art2',
'id': 'db721fb0-5b85-4738-9401-f161d541de5e',
'version': '0.0.0'
}],
'next': '/artifacts/images?'
'marker=e1090471-1d12-4935-a8d8-a9351266ece8&limit=2'},
),
},
'/artifacts/images?'
'limit=2&marker=e1090471-1d12-4935-a8d8-a9351266ece8': {
'GET': (
{},
{'images': [
{
'name': 'art3',
'id': 'e1090471-1d12-4935-a8d8-a9351266ece8',
'version': '0.0.0'
}
]},
),
},
'/artifacts/images?limit=20&sort=name%3Adesc': {
'GET': (
{},
{'images': [
{
'name': 'art2',
'id': 'e4f027d2-bff3-4084-a2ba-f31cb5e3067f',
'version': '0.0.0'
},
{
'name': 'art1',
'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
'version': '0.0.0'
}
],
'next': '/artifacts/images?'
'marker=3a4560a1-e585-443e-9b39-553b46ec92d1&limit=20'},
),
},
'/artifacts/images?limit=20&sort=name': {
'GET': (
{},
{'images': [
{
'name': 'art2',
'id': 'e4f027d2-bff3-4084-a2ba-f31cb5e3067f',
'version': '0.0.0'
},
{
'name': 'art1',
'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
'version': '0.0.0'
}
],
'next': '/artifacts/images?'
'marker=3a4560a1-e585-443e-9b39-553b46ec92d1&limit=20'},
),
},
'/artifacts/images?'
'limit=20&marker=3a4560a1-e585-443e-9b39-553b46ec92d1': {
'GET': (
{},
{'images': [
{
'name': 'art1',
'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
'version': '0.0.0'
}
]}
),
},
'/artifacts/images': {
'POST': (
{},
{'images': [
{
'name': 'art_1',
'id': '3a4560a1-e585-443e-9b39-553b46ec92a3',
'version': '0.0.0'
}
]}
),
},
'/artifacts/images/3a4560a1-e585-443e-9b39-553b46ec92a3': {
'DELETE': (
{},
{}
),
'PATCH': (
{},
''
),
'GET': (
{},
{'images': [
{
'name': 'art_1',
'id': '3a4560a1-e585-443e-9b39-553b46ec92a3',
'version': '0.0.0'
}
]}
)
},
'/artifacts/images/3a4560a1-e585-443e-9b39-553b46ec92a3/image': {
'PUT': (
{},
''
),
'GET': (
{'content-md5': '5cc4bebc-db27-11e1-a1eb-080027cbe205'},
{}
)
},
'/artifacts/images/3a4560a1-e585-443e-9b39-553b46ec92a2/image': {
'PUT': (
{},
''
),
'GET': (
{'content-md5': '5cc4bebc-db27-11e1-a1eb-080027cbe205'},
{}
)
},
'/artifacts/images/3a4560a1-e585-443e-9b39-553b46ec92a8/image': {
'PUT': (
{},
''
),
},
'/schemas': {
'GET': (
{},
{'schemas': {
'images': {'name': 'images', 'version': '1.0'},
'heat_environments': {'name': 'heat_environments',
'version': '1.0'}}}
)
},
'/schemas/images': {
'GET': (
{},
{'schemas': {
'images': {'name': 'images',
'version': '1.0',
'properties': {'foo': 'bar'}
}}}
)
},
'/artifacts/images?name=name1&version=latest': {
'GET': (
# headers
{},
# response
{'images': [
{
'name': 'name1',
'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
'version': '3.0.0'
}
]},
),
},
'/artifacts/images?name=name1&version=1.0.0': {
'GET': (
# headers
{},
# response
{'images': [
{
'name': 'name1',
'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
'version': '1.0.0'
}
]},
),
},
'/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c': {
'PATCH': (
{},
''
),
'GET': (
{},
{
'name': 'art_1',
'id': '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
'version': '0.0.0',
'tags': ["a", "b", "c"]
}
)
},
}

View File

@@ -13,454 +13,194 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import mock
from oslo_serialization import jsonutils
import testtools import testtools
from glareclient.exc import HTTPBadRequest
from glareclient.tests.unit.v1 import fixtures
from glareclient.tests import utils
from glareclient.v1 import artifacts from glareclient.v1 import artifacts
class TestController(testtools.TestCase): class TestController(testtools.TestCase):
def setUp(self): def setUp(self):
self.mock_resp = mock.MagicMock()
self.mock_body = mock.MagicMock()
self.mock_http_client = mock.MagicMock()
for method in ('get', 'post', 'patch', 'delete'):
method = getattr(self.mock_http_client, method)
method.return_value = (self.mock_resp, self.mock_body)
self.c = artifacts.Controller(self.mock_http_client, 'test_name')
self.c._check_type_name = mock.Mock(return_value='checked_name')
super(TestController, self).setUp() super(TestController, self).setUp()
self.api = utils.FakeAPI(fixtures.data_fixtures)
self.controller = artifacts.Controller(self.api)
def test_list_artifacts(self): def test_create(self):
artifacts = list(self.controller.list(type_name='images')) body = self.c.create('name', version='0.1.2', type_name='ok')
self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', self.assertEqual(self.mock_body, body)
artifacts[0]['id']) self.mock_http_client.post.assert_called_once_with(
self.assertEqual('art1', artifacts[0]['name']) '/artifacts/checked_name',
self.assertEqual('db721fb0-5b85-4738-9401-f161d541de5e', json={'version': '0.1.2', 'name': 'name'})
artifacts[1]['id']) self.c._check_type_name.assert_called_once_with('ok')
self.assertEqual('art2', artifacts[1]['name'])
self.assertEqual('e4f027d2-bff3-4084-a2ba-f31cb5e3067f',
artifacts[2]['id'])
self.assertEqual('art3', artifacts[2]['name'])
exp_headers = {} def test_update(self):
expect_body = None remove_props = ['remove1', 'remove2']
expect = [('GET', '/artifacts/images?limit=20', body = self.c.update('test-id', type_name='test_name',
exp_headers, remove_props=remove_props, update1=1, update2=2)
expect_body)] self.assertEqual(self.mock_body, body)
self.assertEqual(expect, self.api.calls) patch_kwargs = {
'headers': {'Content-Type': 'application/json-patch+json'},
def test_list_with_paginate(self): 'json': [
artifacts = list(self.controller.list(type_name='images', {'path': '/remove1', 'value': None, 'op': 'replace'},
page_size=2)) {'path': '/remove2', 'value': None, 'op': 'replace'},
self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', {'path': '/update2', 'value': 2, 'op': 'add'},
artifacts[0]['id']) {'path': '/update1', 'value': 1, 'op': 'add'}
self.assertEqual('art1', artifacts[0]['name']) ]
self.assertEqual('art2', artifacts[1]['name'])
self.assertEqual('db721fb0-5b85-4738-9401-f161d541de5e',
artifacts[1]['id'])
exp_headers = {}
expect_body = None
expect = [('GET', '/artifacts/images?limit=2',
exp_headers,
expect_body),
('GET', '/artifacts/images?limit=2'
'&marker=e1090471-1d12-4935-a8d8-a9351266ece8',
exp_headers,
expect_body)]
self.assertEqual(expect, self.api.calls)
def test_list_artifacts_limit(self):
artifacts = list(self.controller.list(type_name='images',
limit=2))
self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1',
artifacts[0]['id'])
self.assertEqual('art1', artifacts[0]['name'])
self.assertEqual('art2', artifacts[1]['name'])
self.assertEqual('db721fb0-5b85-4738-9401-f161d541de5e',
artifacts[1]['id'])
exp_headers = {}
expect_body = None
expect = [('GET', '/artifacts/images?limit=2',
exp_headers,
expect_body)]
self.assertEqual(expect, self.api.calls)
def test_list_artifact_sort_name(self):
artifacts = list(self.controller.list(type_name='images',
sort='name:desc'))
self.assertEqual('e4f027d2-bff3-4084-a2ba-f31cb5e3067f',
artifacts[0]['id'])
self.assertEqual('art2', artifacts[0]['name'])
self.assertEqual('art1', artifacts[1]['name'])
self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1',
artifacts[1]['id'])
exp_headers = {}
expect_body = None
expect = [('GET', '/artifacts/images?limit=20'
'&sort=name%3Adesc',
exp_headers,
expect_body),
('GET', '/artifacts/images?limit=20'
'&marker=3a4560a1-e585-443e-9b39-553b46ec92d1',
exp_headers,
expect_body)]
self.assertEqual(expect, self.api.calls)
def test_list_artifact_sort_badrequest(self):
with testtools.ExpectedException(HTTPBadRequest):
list(self.controller.list(type_name='images',
sort='name:KAK'))
def test_create_artifact(self):
properties = {
'name': 'art_1',
'type_name': 'images'
} }
self.mock_http_client.patch.assert_called_once_with(
'/artifacts/checked_name/test-id', **patch_kwargs)
self.c._check_type_name.assert_called_once_with('test_name')
art = self.controller.create(**properties) def test_get(self):
self.assertEqual('art_1', art['images'][0]['name']) body = self.c.get('test-id', type_name='test_name')
self.assertEqual('0.0.0', art['images'][0]['version']) self.assertEqual(self.mock_body, body)
self.assertIsNotNone(art['images'][0]['id']) self.mock_http_client.get.assert_called_once_with(
exp_headers = {} '/artifacts/checked_name/test-id')
expect_body = [('name', 'art_1'), ('version', '0.0.0')] self.c._check_type_name.assert_called_once_with('test_name')
expect = [('POST', '/artifacts/images',
exp_headers,
expect_body)]
self.assertEqual(expect, self.api.calls)
def test_create_artifact_bad_prop(self): def test_list(self):
properties = { self.mock_http_client.get.side_effect = [
'name': 'art_1', (None, {'checked_name': [10, 11, 12], "next": "next1"}),
'type_name': 'bad_type_name', (None, {'checked_name': [13, 14, 15], "next": "next2"}),
} (None, {'checked_name': [16, 17, 18], "next": "next3"}),
with testtools.ExpectedException(KeyError): (None, {'checked_name': [19, 20, 21]}),
self.controller.create(**properties) ]
data = list(self.c.list(type_name='test-type', limit=10, page_size=3))
expected = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
self.assertEqual(expected, data)
expected_calls = [
mock.call.get('/artifacts/checked_name?&limit=3'),
mock.call.get('next1'),
mock.call.get('next2'),
mock.call.get('next3'),
]
self.assertEqual(expected_calls, self.mock_http_client.mock_calls)
def test_delete_artifact(self): def test_activate(self):
self.controller.delete( self.c.update = mock.Mock()
artifact_id='3a4560a1-e585-443e-9b39-553b46ec92a3', self.assertEqual(self.c.update.return_value,
type_name='images') self.c.activate('test-id', type_name='test-type'))
self.c.update.assert_called_once_with('test-id', 'test-type',
status='active')
expect = [('DELETE', '/artifacts/images/' def test_deactivate(self):
'3a4560a1-e585-443e-9b39-553b46ec92a3', self.c.update = mock.Mock()
{}, self.assertEqual(self.c.update.return_value,
None)] self.c.deactivate('test-id', type_name='test-type'))
self.assertEqual(expect, self.api.calls) self.c.update.assert_called_once_with('test-id', 'test-type',
status='deactivated')
def test_update_prop(self): def test_reactivate(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3' self.c.update = mock.Mock()
param = {'type_name': 'images', self.assertEqual(self.c.update.return_value,
'name': 'new_name'} self.c.reactivate('test-id', type_name='test-type'))
self.c.update.assert_called_once_with('test-id', 'test-type',
status='active')
self.controller.update(artifact_id=art_id, def test_delete(self):
**param) self.assertEqual(None, self.c.delete('test-id', type_name='test-name'))
self.mock_http_client.delete.assert_called_once_with(
exp_headers = { '/artifacts/checked_name/test-id')
'Content-Type': 'application/json-patch+json'
}
expect_body = [{'path': '/name',
'value': 'new_name',
'op': 'add'}]
expect = [('PATCH', '/artifacts/images/%s' % art_id,
exp_headers,
expect_body)]
self.assertEqual(expect, self.api.calls)
def test_remove_prop(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
self.controller.update(artifact_id=art_id,
remove_props=['name'],
type_name='images')
exp_headers = {
'Content-Type': 'application/json-patch+json'
}
expect_body = [{'path': '/name',
'op': 'replace',
'value': None}]
expect = [('PATCH', '/artifacts/images/%s' % art_id,
exp_headers,
expect_body)]
self.assertEqual(expect, self.api.calls)
self.api.calls = []
self.controller.update(artifact_id=art_id,
remove_props=['metadata/key1'],
type_name='images')
expect_body = [{'path': '/metadata/key1',
'op': 'remove'}]
expect = [('PATCH', '/artifacts/images/%s' % art_id,
exp_headers,
expect_body)]
self.assertEqual(expect, self.api.calls)
self.api.calls = []
self.controller.update(artifact_id=art_id,
remove_props=['releases/1'],
type_name='images')
expect_body = [{'path': '/releases/1',
'op': 'remove'}]
expect = [('PATCH', '/artifacts/images/%s' % art_id,
exp_headers,
expect_body)]
self.assertEqual(expect, self.api.calls)
def test_nontype_type_name(self):
with testtools.ExpectedException(HTTPBadRequest):
self.controller.create(name='art')
def test_active_artifact(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
self.controller.activate(artifact_id=art_id,
type_name='images')
exp_headers = {
'Content-Type': 'application/json-patch+json'
}
expect_body = [{'path': '/status',
'value': 'active',
'op': 'add'}]
expect = [('PATCH', '/artifacts/images/%s' % art_id,
exp_headers,
expect_body)]
self.assertEqual(expect, self.api.calls)
def test_deactivate_artifact(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
self.controller.deactivate(artifact_id=art_id,
type_name='images')
exp_headers = {
'Content-Type': 'application/json-patch+json'
}
expect_body = [{'path': '/status',
'value': 'deactivated',
'op': 'add'}]
expect = [('PATCH', '/artifacts/images/%s' % art_id,
exp_headers,
expect_body)]
self.assertEqual(expect, self.api.calls)
def test_reactivate_artifact(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
self.controller.reactivate(artifact_id=art_id,
type_name='images')
exp_headers = {
'Content-Type': 'application/json-patch+json'
}
expect_body = [{'path': '/status',
'value': 'active',
'op': 'add'}]
expect = [('PATCH', '/artifacts/images/%s' % art_id,
exp_headers,
expect_body)]
self.assertEqual(expect, self.api.calls)
def test_publish_artifact(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
self.controller.publish(artifact_id=art_id,
type_name='images')
exp_headers = {
'Content-Type': 'application/json-patch+json'
}
expect_body = [{'path': '/visibility',
'value': 'public',
'op': 'add'}]
expect = [('PATCH', '/artifacts/images/%s' % art_id,
exp_headers,
expect_body)]
self.assertEqual(expect, self.api.calls)
def test_upload_blob(self): def test_upload_blob(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3' self.c.upload_blob('test-id', 'blob-prop', 'data',
self.controller.upload_blob(artifact_id=art_id, type_name='test-type',
type_name='images', content_type='application/test')
blob_property='image', self.mock_http_client.put.assert_called_once_with(
data='data') '/artifacts/checked_name/test-id/blob-prop',
data='data', headers={'Content-Type': 'application/test'})
exp_headers = { def test_get_type_list(self):
'Content-Type': 'application/octet-stream' schemas = {'schemas': {'a': {'version': 1}, 'b': {'version': 2}}}
} self.mock_http_client.get.return_value = (None, schemas)
expected_types = [('a', 1), ('b', 2)]
self.assertEqual(expected_types, self.c.get_type_list())
expect = [('PUT', '/artifacts/images/%s/image' % art_id, def test_get_type_schema(self):
exp_headers, test_schema = {'schemas': {'checked_name': 'test-schema'}}
'data')] self.mock_http_client.get.return_value = (None, test_schema)
self.assertEqual(expect, self.api.calls) self.assertEqual('test-schema',
self.c.get_type_schema(type_name='test-type'))
def test_upload_blob_custom_content_type(self): self.mock_http_client.get.assert_called_once_with(
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3' '/schemas/checked_name')
self.controller.upload_blob(artifact_id=art_id,
type_name='images',
blob_property='image',
data='{"a":"b"}',
content_type='application/json',)
exp_headers = {
'Content-Type': 'application/json'
}
expect = [('PUT', '/artifacts/images/%s/image' % art_id,
exp_headers,
{"a": "b"})]
self.assertEqual(expect, self.api.calls)
def test_download_blob(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
self.controller.download_blob(artifact_id=art_id,
type_name='images',
blob_property='image')
exp_headers = {}
expect = [('GET', '/artifacts/images/%s/image' % art_id,
exp_headers,
None)]
self.assertEqual(expect, self.api.calls)
def test_download_blob_with_checksum(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a2'
data = self.controller.download_blob(artifact_id=art_id,
type_name='images',
blob_property='image')
self.assertIsNotNone(data.iterable)
expect = [('GET', '/artifacts/images/%s/image' % art_id,
{},
None)]
self.assertEqual(expect, self.api.calls)
def test_download_blob_without_checksum(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a2'
data = self.controller.download_blob(artifact_id=art_id,
type_name='images',
blob_property='image',
do_checksum=False)
self.assertIsNotNone(data.iterable)
expect = [('GET', '/artifacts/images/%s/image' % art_id,
{},
None)]
self.assertEqual(expect, self.api.calls)
def test_get_artifact(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
art = self.controller.get(artifact_id=art_id,
type_name='images')
self.assertEqual(art_id, art['images'][0]['id'])
self.assertEqual('art_1', art['images'][0]['name'])
def test_get_by_name(self):
art_name = 'name1'
art = self.controller.get_by_name(name=art_name,
type_name='images')
self.assertEqual(art_name, art['name'])
self.assertEqual('3.0.0', art['version'])
def test_get_by_name_with_version(self):
art_name = 'name1'
art = self.controller.get_by_name(name=art_name,
version='1.0.0',
type_name='images')
self.assertEqual(art_name, art['name'])
self.assertEqual('1.0.0', art['version'])
def test_type_list(self):
data = self.controller.get_type_list()
expect_data = [('images', '1.0'), ('heat_environments', '1.0')]
expect_call = [('GET', '/schemas', {}, None)]
self.assertEqual(expect_call, self.api.calls)
self.assertEqual(expect_data, data)
def test_get_schema(self):
data = self.controller.get_type_schema(type_name='images')
expect_data = {'name': 'images', 'version': '1.0',
'properties': {'foo': 'bar'}}
expect_call = [('GET', '/schemas/images', {}, None)]
self.assertEqual(expect_call, self.api.calls)
self.assertEqual(expect_data, data)
def test_add_external_location(self): def test_add_external_location(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a8' art_id = '3a4560a1-e585-443e-9b39-553b46ec92a8'
data = self.controller.add_external_location(art_id, data = {
'image', 'url': 'http://fake_url',
'http://fake_url', 'md5': '7CA772EE98D5CAF99F3674085D5E4124',
type_name='images') 'sha1': None,
expect_call = [ 'sha256': None},
('PUT', resp = self.c.add_external_location(
'/artifacts/images/3a4560a1-e585-443e-9b39-553b46ec92a8/image', art_id, 'image',
{'Content-Type': 'application/vnd+openstack.' data=data,
'glare-custom-location+json'}, type_name='images')
'http://fake_url')] self.c.http_client.put.assert_called_once_with(
self.assertEqual(expect_call, self.api.calls) '/artifacts/checked_name/'
self.assertIsNone(data) '3a4560a1-e585-443e-9b39-553b46ec92a8/image',
data=jsonutils.dumps(data),
headers={'Content-Type':
'application/vnd+openstack.glare-custom-location+json'})
self.assertIsNone(resp)
def test_add_tag(self): def test_add_tag(self):
art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c' art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c'
data = self.controller.add_tag( d = {'tags': ['a', 'b', 'c']}
self.mock_body.__getitem__.side_effect = d.__getitem__
data = self.c.add_tag(
art_id, tag_value="123", type_name='images') art_id, tag_value="123", type_name='images')
expect_call = [ self.c.http_client.get.assert_called_once_with(
('GET', '/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c', '/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c')
{}, None), self.c.http_client.patch.assert_called_once_with(
('PATCH', '/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
'/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c', headers={'Content-Type': 'application/json-patch+json'},
{'Content-Type': 'application/json-patch+json'}, json=[{'path': '/tags',
[{'op': 'add', 'value': ['a', 'b', 'c', '123'],
'path': '/tags', 'op': 'add'}])
'value': ['a', 'b', 'c', '123']}])]
self.assertEqual(expect_call, self.api.calls)
self.assertIsNotNone(data) self.assertIsNotNone(data)
def test_add_existing_tag(self): def test_add_existing_tag(self):
art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c' art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c'
data = self.controller.add_tag( d = {'tags': ['a', 'b', 'c']}
self.mock_body.__getitem__.side_effect = d.__getitem__
data = self.c.add_tag(
art_id, tag_value="a", type_name='images') art_id, tag_value="a", type_name='images')
expect_call = [ self.c.http_client.get.assert_called_once_with(
('GET', '/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c', '/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c')
{}, None)] self.assertEqual(0, self.c.http_client.patch.call_count)
self.assertEqual(expect_call, self.api.calls)
self.assertIsNotNone(data) self.assertIsNotNone(data)
def test_remove_tag(self): def test_remove_tag(self):
art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c' art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c'
data = self.controller.remove_tag( d = {'tags': ['a', 'b', 'c']}
self.mock_body.__getitem__.side_effect = d.__getitem__
data = self.c.remove_tag(
art_id, tag_value="a", type_name='images') art_id, tag_value="a", type_name='images')
expect_call = [ self.c.http_client.get.assert_called_once_with(
('GET', '/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c', '/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c')
{}, None), self.c.http_client.patch.assert_called_once_with(
('PATCH', '/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
'/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c', headers={'Content-Type': 'application/json-patch+json'},
{'Content-Type': 'application/json-patch+json'}, json=[{'path': '/tags',
[{'op': 'add', 'value': ['b', 'c'],
'path': '/tags', 'op': 'add'}])
'value': ['b', 'c']}])]
self.assertEqual(expect_call, self.api.calls)
self.assertIsNotNone(data) self.assertIsNotNone(data)
def test_remove_nonexisting_tag(self): def test_remove_nonexisting_tag(self):
art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c' art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c'
data = self.controller.remove_tag( d = {'tags': ['a', 'b', 'c']}
self.mock_body.__getitem__.side_effect = d.__getitem__
data = self.c.remove_tag(
art_id, tag_value="123", type_name='images') art_id, tag_value="123", type_name='images')
expect_call = [ self.c.http_client.get.assert_called_once_with(
('GET', '/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c', '/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c')
{}, None)] self.assertEqual(0, self.c.http_client.patch.call_count)
self.assertEqual(expect_call, self.api.calls)
self.assertIsNotNone(data) self.assertIsNotNone(data)

View File

@@ -1,213 +0,0 @@
# Copyright 2012 OpenStack Foundation
# All 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 copy
import json
import six
import six.moves.urllib.parse as urlparse
import testtools
class FakeAPI(object):
def __init__(self, fixtures):
self.fixtures = fixtures
self.calls = []
def _request(self, method, url, headers=None, data=None,
content_length=None, **kwargs):
call = build_call_record(method, sort_url_by_query_keys(url),
headers or {}, data)
if content_length is not None:
call = tuple(list(call) + [content_length])
self.calls.append(call)
fixture = self.fixtures[sort_url_by_query_keys(url)][method]
data = fixture[1]
if isinstance(fixture[1], six.string_types):
try:
data = json.loads(fixture[1])
except ValueError:
data = six.StringIO(fixture[1])
return FakeResponse(fixture[0], fixture[1]), data
def get(self, *args, **kwargs):
return self._request('GET', *args, **kwargs)
def post(self, *args, **kwargs):
return self._request('POST', *args, **kwargs)
def put(self, *args, **kwargs):
return self._request('PUT', *args, **kwargs)
def patch(self, *args, **kwargs):
return self._request('PATCH', *args, **kwargs)
def delete(self, *args, **kwargs):
return self._request('DELETE', *args, **kwargs)
def head(self, *args, **kwargs):
return self._request('HEAD', *args, **kwargs)
class RawRequest(object):
def __init__(self, headers, body=None,
version=1.0, status=200, reason="Ok"):
"""A crafted request object used for testing.
:param headers: dict representing HTTP response headers
:param body: file-like object
:param version: HTTP Version
:param status: Response status code
:param reason: Status code related message.
"""
self.body = body
self.status = status
self.reason = reason
self.version = version
self.headers = headers
def getheaders(self):
return copy.deepcopy(self.headers).items()
def getheader(self, key, default):
return self.headers.get(key, default)
def read(self, amt):
return self.body.read(amt)
class FakeResponse(object):
def __init__(self, headers=None, body=None,
version=1.0, status_code=200, reason="Ok"):
"""A crafted response object used for testing.
:param headers: dict representing HTTP response headers
:param body: file-like object
:param version: HTTP Version
:param status: Response status code
:param reason: Status code related message.
"""
self.body = body
self.reason = reason
self.version = version
self.headers = headers
self.status_code = status_code
self.raw = RawRequest(headers, body=body, reason=reason,
version=version, status=status_code)
@property
def status(self):
return self.status_code
@property
def ok(self):
return (self.status_code < 400 or
self.status_code >= 600)
def read(self, amt):
return self.body.read(amt)
def close(self):
pass
@property
def content(self):
if hasattr(self.body, "read"):
return self.body.read()
return self.body
@property
def text(self):
if isinstance(self.content, six.binary_type):
return self.content.decode('utf-8')
return self.content
def json(self, **kwargs):
return self.body and json.loads(self.text) or ""
def iter_content(self, chunk_size=1, decode_unicode=False):
while True:
chunk = self.raw.read(chunk_size)
if not chunk:
break
yield chunk
def release_conn(self, **kwargs):
pass
class TestCase(testtools.TestCase):
TEST_REQUEST_BASE = {
'config': {'danger_mode': False},
'verify': True}
class FakeTTYStdout(six.StringIO):
"""A Fake stdout that try to emulate a TTY device as much as possible."""
def isatty(self):
return True
def write(self, data):
# When a CR (carriage return) is found reset file.
if data.startswith('\r'):
self.seek(0)
data = data[1:]
return six.StringIO.write(self, data)
class FakeNoTTYStdout(FakeTTYStdout):
"""A Fake stdout that is not a TTY device."""
def isatty(self):
return False
def sort_url_by_query_keys(url):
"""A helper function which sorts the keys of the query string of a url.
For example, an input of '/v2/tasks?sort_key=id&sort_dir=asc&limit=10'
returns '/v2/tasks?limit=10&sort_dir=asc&sort_key=id'. This is to
prevent non-deterministic ordering of the query string causing
problems with unit tests.
:param url: url which will be ordered by query keys
:returns url: url with ordered query keys
"""
parsed = urlparse.urlparse(url)
queries = urlparse.parse_qsl(parsed.query, True)
sorted_query = sorted(queries, key=lambda x: x[0])
encoded_sorted_query = urlparse.urlencode(sorted_query, True)
url_parts = (parsed.scheme, parsed.netloc, parsed.path,
parsed.params, encoded_sorted_query,
parsed.fragment)
return urlparse.urlunparse(url_parts)
def build_call_record(method, url, headers, data):
"""Key the request body be ordered if it's a dict type."""
if isinstance(data, dict):
data = sorted(data.items())
if isinstance(data, six.string_types):
try:
data = json.loads(data)
except ValueError:
return (method, url, headers or {}, data)
return (method, url, headers or {}, data)

View File

@@ -12,12 +12,14 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from glareclient.common import utils from oslo_serialization import jsonutils
from glareclient import exc
from oslo_utils import encodeutils from oslo_utils import encodeutils
import six import six
from six.moves.urllib import parse from six.moves.urllib import parse
from glareclient.common import utils
from glareclient import exc
class Controller(object): class Controller(object):
def __init__(self, http_client, type_name=None): def __init__(self, http_client, type_name=None):
@@ -59,7 +61,7 @@ class Controller(object):
type_name = self._check_type_name(type_name) type_name = self._check_type_name(type_name)
kwargs.update({'name': name, 'version': version}) kwargs.update({'name': name, 'version': version})
url = '/artifacts/%s' % type_name url = '/artifacts/%s' % type_name
resp, body = self.http_client.post(url, data=kwargs) resp, body = self.http_client.post(url, json=kwargs)
return body return body
def update(self, artifact_id, type_name=None, remove_props=None, def update(self, artifact_id, type_name=None, remove_props=None,
@@ -90,7 +92,7 @@ class Controller(object):
for prop_name in kwargs: for prop_name in kwargs:
changes.append({'op': 'add', 'path': '/%s' % prop_name, changes.append({'op': 'add', 'path': '/%s' % prop_name,
'value': kwargs[prop_name]}) 'value': kwargs[prop_name]})
resp, body = self.http_client.patch(url, headers=hdrs, data=changes) resp, body = self.http_client.patch(url, headers=hdrs, json=changes)
return body return body
def get(self, artifact_id, type_name=None): def get(self, artifact_id, type_name=None):
@@ -226,7 +228,7 @@ class Controller(object):
type_name = self._check_type_name(type_name) type_name = self._check_type_name(type_name)
hdrs = {'Content-Type': content_type} hdrs = {'Content-Type': content_type}
url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property) url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
self.http_client.put(url, headers=hdrs, data=data, stream=True) self.http_client.put(url, headers=hdrs, data=data)
def add_external_location(self, artifact_id, blob_property, data, def add_external_location(self, artifact_id, blob_property, data,
type_name=None): type_name=None):
@@ -240,6 +242,10 @@ class Controller(object):
type_name = self._check_type_name(type_name) type_name = self._check_type_name(type_name)
hdrs = {'Content-Type': content_type} hdrs = {'Content-Type': content_type}
url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property) url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
try:
data = jsonutils.dumps(data)
except TypeError:
raise exc.HTTPBadRequest("json is malformed.")
self.http_client.put(url, headers=hdrs, data=data) self.http_client.put(url, headers=hdrs, data=data)
def download_blob(self, artifact_id, blob_property, type_name=None, def download_blob(self, artifact_id, blob_property, type_name=None,
@@ -252,12 +258,10 @@ class Controller(object):
""" """
type_name = self._check_type_name(type_name) type_name = self._check_type_name(type_name)
url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property) url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
resp, body = self.http_client.get(url) resp, body = self.http_client.get(url, redirect=False,
checksum = resp.headers.get('content-md5', None) stream=True,
content_length = int(resp.headers.get('content-length', 0)) headers={"Accept": "*/*"})
if checksum is not None and do_checksum: return utils.ResponseBlobWrapper(resp, do_checksum)
body = utils.integrity_iter(body, checksum)
return utils.IterableWithLength(body, content_length)
def get_type_list(self): def get_type_list(self):
"""Get list of type names.""" """Get list of type names."""