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
from six.moves import urllib
from glareclient._i18n import _
from glareclient.common import exceptions as exc
LOG = logging.getLogger(__name__)
@ -52,43 +51,6 @@ def get_system_ca_file():
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):
content_type = resp.headers.get('Content-Type')
if not content_type:
@ -107,17 +69,6 @@ def _handle_response(resp):
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):
def __init__(self, endpoint, **kwargs):
@ -305,8 +256,7 @@ class HTTPClient(object):
return creds
def json_request(self, url, method, **kwargs):
params = _set_request_params(kwargs)
resp = self.request(url, method, **params)
resp = self.request(url, method, **kwargs)
return _handle_response(resp)
def json_patch_request(self, url, method='PATCH', **kwargs):
@ -336,37 +286,14 @@ class SessionClient(adapter.LegacyJsonAdapter):
"""HTTP client based on Keystone client session."""
def request(self, url, method, **kwargs):
params = _set_request_params(kwargs)
redirect = kwargs.get('redirect')
resp, body = super(SessionClient, self).request(
url, method,
**params)
url, method, **kwargs)
if 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:
if resp.status_code == 300 or (400 <= resp.status_code < 600):
raise exc.from_response(resp)
if resp.headers.get('Content-Type') == 'application/octet-stream':
body = _close_after_stream(resp, CHUNKSIZE)
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):
session = kwargs.pop('session', None)

View File

@ -13,9 +13,12 @@
# License for the specific language governing permissions and limitations
# under the License.
import decimal
import os
import sys
import six
SPIN_CHARS = ('-', '\\', '|', '/')
CHUNKSIZE = 1024 * 64 # 64kB
class _ProgressBarBase(object):
@ -30,21 +33,37 @@ class _ProgressBarBase(object):
: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._totalsize = float(totalsize)
self._show_progress = sys.stdout.isatty()
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):
self._totalread += size_read
if self._show_progress:
if self._totalsize == 0:
self._totalsize = size_read
self._percent += size_read / self._totalsize
# Output something like this: [==========> ] 49%
sys.stdout.write('\r[{0:<30}] {1:.0%}'.format(
'=' * int(round(self._percent * 29)) + '>', self._percent
))
if self._totalsize:
percent = float(self._totalread) / float(self._totalsize)
# Output something like this: [==========> ] 49%
sys.stdout.write('\r[{0:<30}] {1:.0%}'.format(
'=' * int(decimal.Decimal(percent * 29).quantize(
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()
def __getattr__(self, attr):
@ -70,24 +89,12 @@ class VerboseFileWrapper(_ProgressBarBase):
sys.stdout.write('\n')
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):
return self
def next(self):
try:
data = six.next(self._wrapped)
# NOTE(mouad): Assuming that data is a string b/c otherwise calling
# len function will not make any sense.
data = self._wrapped.next()
self._display_progress_bar(len(data))
return data
except StopIteration:

View File

@ -18,7 +18,6 @@ from __future__ import print_function
import errno
import hashlib
import os
import six
import sys
if os.name == 'nt':
@ -28,6 +27,7 @@ else:
from oslo_utils import encodeutils
from oslo_utils import importutils
import requests
SENSITIVE_HEADERS = ('X-Auth-Token', )
@ -58,41 +58,40 @@ def exit(msg='', exit_code=1):
sys.exit(exit_code)
def integrity_iter(iter, checksum):
"""Check blob integrity.
class ResponseBlobWrapper(object):
"""Represent HTTP response as iterator with known length."""
:raises: IOError
"""
md5sum = hashlib.md5()
for chunk in iter:
yield chunk
if isinstance(chunk, six.string_types):
chunk = six.b(chunk)
md5sum.update(chunk)
md5sum = md5sum.hexdigest()
if md5sum != checksum:
raise IOError(errno.EPIPE,
'Corrupt blob download. Checksum was %s expected %s' %
(md5sum, checksum))
class IterableWithLength(object):
def __init__(self, iterable, length):
self.iterable = iterable
self.length = length
def __init__(self, resp, verify_md5=True):
self.hash_md5 = resp.headers.get("Content-MD5")
self.check_md5 = hashlib.md5()
if 301 <= resp.status_code <= 302:
# NOTE(sskripnick): handle redirect manually to prevent sending
# auth token to external resource.
# Use stream=True to prevent reading whole response into memory.
# Set Accept-Encoding explicitly to "identity" because setting
# stream=True forces Accept-Encoding to be "gzip, defalate".
# It should be "identity" because we should know Content-Length.
resp = requests.get(resp.headers.get("Location"),
headers={"Accept-Encoding": "identity"})
self.len = resp.headers.get("Content-Length", 0)
self.iter = resp.iter_content(65536)
def __iter__(self):
try:
for chunk in self.iterable:
yield chunk
finally:
self.iterable.close()
return 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):
return self.length
__next__ = next
def get_item_properties(item, fields, mixed_case_fields=None, formatters=None):
@ -158,60 +157,3 @@ def save_blob(data, path):
finally:
if path is not None:
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 utils
from glareclient import exc
from glareclient.osc.v1 import TypeMapperAction
LOG = logging.getLogger(__name__)
@ -107,11 +108,15 @@ class UploadBlob(command.ShowOne):
parsed_args.blob_property = _default_blob_property(
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:
file_size = utils.get_file_size(blob)
if file_size is not None:
blob = progressbar.VerboseFileWrapper(blob, file_size)
blob = progressbar.VerboseFileWrapper(blob)
client.artifacts.upload_blob(af_id, parsed_args.blob_property, blob,
content_type=parsed_args.content_type,
@ -186,7 +191,7 @@ class DownloadBlob(command.Command):
parsed_args.blob_property,
type_name=parsed_args.type_name)
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):
utils.save_blob(data, parsed_args.file)
else:

View File

@ -14,12 +14,108 @@
import mock
from glareclient import exc
from glareclient.osc.v1 import blobs as osc_blob
from glareclient.tests.unit.osc.v1 import fakes
from glareclient.v1 import artifacts as api_art
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):
def setUp(self):
super(TestBlobs, self).setUp()
@ -28,187 +124,6 @@ class TestBlobs(fakes.TestArtifacts):
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):
def setUp(self):
super(TestDownloadBlob, self).setUp()

View File

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

View File

@ -13,63 +13,65 @@
# License for the specific language governing permissions and limitations
# under the License.
import sys
import six
import mock
from six import StringIO
import testtools
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):
size = 100
iterator = iter('X' * 100)
saved_stdout = sys.stdout
try:
sys.stdout = output = test_utils.FakeTTYStdout()
# Consume iterator.
data = list(progressbar.VerboseIteratorWrapper(iterator, size))
self.assertEqual(['X'] * 100, data)
self.assertEqual(
'[%s>] 100%%\n' % ('=' * 29),
output.getvalue()
)
finally:
sys.stdout = saved_stdout
@mock.patch(MOD + '.os')
def test_totalsize_fileno(self, mock_os):
mock_os.fstat.return_value.st_size = 43
fake_file = mock.Mock()
del fake_file.len
fake_file.fileno.return_value = 42
pb = progressbar.VerboseFileWrapper(fake_file)
self.assertEqual(43, pb._totalsize)
mock_os.fstat.assert_called_once_with(42)
def test_iter_file_display_progress_bar(self):
size = 98304
file_obj = six.StringIO('X' * size)
saved_stdout = sys.stdout
try:
sys.stdout = output = test_utils.FakeTTYStdout()
file_obj = progressbar.VerboseFileWrapper(file_obj, size)
chunksize = 1024
chunk = file_obj.read(chunksize)
while chunk:
chunk = file_obj.read(chunksize)
self.assertEqual(
'[%s>] 100%%\n' % ('=' * 29),
output.getvalue()
)
finally:
sys.stdout = saved_stdout
@mock.patch(MOD + '.sys')
def test__display_progress_bar(self, mock_sys):
fake_file = StringIO('test') # 4 bytes
fake_file.len = 4
pb = progressbar.VerboseFileWrapper(fake_file)
pb._display_progress_bar(2) # 2 of 4 bytes = 50%
pb._display_progress_bar(1) # 3 of 4 bytes = 75%
pb._display_progress_bar(1) # 4 of 4 bytes = 100%
expected = [
mock.call('\r[===============> ] 50%'),
mock.call('\r[======================> ] 75%'),
mock.call('\r[=============================>] 100%'),
]
self.assertEqual(expected, mock_sys.stdout.write.mock_calls)
def test_iter_file_no_tty(self):
size = 98304
file_obj = six.StringIO('X' * size)
saved_stdout = sys.stdout
try:
sys.stdout = output = test_utils.FakeNoTTYStdout()
file_obj = progressbar.VerboseFileWrapper(file_obj, size)
chunksize = 1024
chunk = file_obj.read(chunksize)
while chunk:
chunk = file_obj.read(chunksize)
# If stdout is not a tty progress bar should do nothing.
self.assertEqual('', output.getvalue())
finally:
sys.stdout = saved_stdout
@mock.patch(MOD + '.sys')
def test__display_progress_bar_unknown_len(self, mock_sys):
fake_file = StringIO('')
fake_file.len = 0
pb = progressbar.VerboseFileWrapper(fake_file)
for i in range(6):
pb._display_progress_bar(1)
expected = [
mock.call('\r[-] 1 bytes'),
mock.call('\r[\\] 2 bytes'),
mock.call('\r[|] 3 bytes'),
mock.call('\r[/] 4 bytes'),
mock.call('\r[-] 5 bytes'),
mock.call('\r[\\] 6 bytes'),
]
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.
import mock
import six
import testtools
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("9.3MB", utils.make_size_human_readable(9761280))
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
# under the License.
import mock
from oslo_serialization import jsonutils
import testtools
from glareclient.exc import HTTPBadRequest
from glareclient.tests.unit.v1 import fixtures
from glareclient.tests import utils
from glareclient.v1 import artifacts
class TestController(testtools.TestCase):
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()
self.api = utils.FakeAPI(fixtures.data_fixtures)
self.controller = artifacts.Controller(self.api)
def test_list_artifacts(self):
artifacts = list(self.controller.list(type_name='images'))
self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1',
artifacts[0]['id'])
self.assertEqual('art1', artifacts[0]['name'])
self.assertEqual('db721fb0-5b85-4738-9401-f161d541de5e',
artifacts[1]['id'])
self.assertEqual('art2', artifacts[1]['name'])
self.assertEqual('e4f027d2-bff3-4084-a2ba-f31cb5e3067f',
artifacts[2]['id'])
self.assertEqual('art3', artifacts[2]['name'])
def test_create(self):
body = self.c.create('name', version='0.1.2', type_name='ok')
self.assertEqual(self.mock_body, body)
self.mock_http_client.post.assert_called_once_with(
'/artifacts/checked_name',
json={'version': '0.1.2', 'name': 'name'})
self.c._check_type_name.assert_called_once_with('ok')
exp_headers = {}
expect_body = None
expect = [('GET', '/artifacts/images?limit=20',
exp_headers,
expect_body)]
self.assertEqual(expect, self.api.calls)
def test_list_with_paginate(self):
artifacts = list(self.controller.list(type_name='images',
page_size=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),
('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'
def test_update(self):
remove_props = ['remove1', 'remove2']
body = self.c.update('test-id', type_name='test_name',
remove_props=remove_props, update1=1, update2=2)
self.assertEqual(self.mock_body, body)
patch_kwargs = {
'headers': {'Content-Type': 'application/json-patch+json'},
'json': [
{'path': '/remove1', 'value': None, 'op': 'replace'},
{'path': '/remove2', 'value': None, 'op': 'replace'},
{'path': '/update2', 'value': 2, 'op': 'add'},
{'path': '/update1', 'value': 1, 'op': 'add'}
]
}
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)
self.assertEqual('art_1', art['images'][0]['name'])
self.assertEqual('0.0.0', art['images'][0]['version'])
self.assertIsNotNone(art['images'][0]['id'])
exp_headers = {}
expect_body = [('name', 'art_1'), ('version', '0.0.0')]
expect = [('POST', '/artifacts/images',
exp_headers,
expect_body)]
self.assertEqual(expect, self.api.calls)
def test_get(self):
body = self.c.get('test-id', type_name='test_name')
self.assertEqual(self.mock_body, body)
self.mock_http_client.get.assert_called_once_with(
'/artifacts/checked_name/test-id')
self.c._check_type_name.assert_called_once_with('test_name')
def test_create_artifact_bad_prop(self):
properties = {
'name': 'art_1',
'type_name': 'bad_type_name',
}
with testtools.ExpectedException(KeyError):
self.controller.create(**properties)
def test_list(self):
self.mock_http_client.get.side_effect = [
(None, {'checked_name': [10, 11, 12], "next": "next1"}),
(None, {'checked_name': [13, 14, 15], "next": "next2"}),
(None, {'checked_name': [16, 17, 18], "next": "next3"}),
(None, {'checked_name': [19, 20, 21]}),
]
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):
self.controller.delete(
artifact_id='3a4560a1-e585-443e-9b39-553b46ec92a3',
type_name='images')
def test_activate(self):
self.c.update = mock.Mock()
self.assertEqual(self.c.update.return_value,
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/'
'3a4560a1-e585-443e-9b39-553b46ec92a3',
{},
None)]
self.assertEqual(expect, self.api.calls)
def test_deactivate(self):
self.c.update = mock.Mock()
self.assertEqual(self.c.update.return_value,
self.c.deactivate('test-id', type_name='test-type'))
self.c.update.assert_called_once_with('test-id', 'test-type',
status='deactivated')
def test_update_prop(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
param = {'type_name': 'images',
'name': 'new_name'}
def test_reactivate(self):
self.c.update = mock.Mock()
self.assertEqual(self.c.update.return_value,
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,
**param)
exp_headers = {
'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_delete(self):
self.assertEqual(None, self.c.delete('test-id', type_name='test-name'))
self.mock_http_client.delete.assert_called_once_with(
'/artifacts/checked_name/test-id')
def test_upload_blob(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
self.controller.upload_blob(artifact_id=art_id,
type_name='images',
blob_property='image',
data='data')
self.c.upload_blob('test-id', 'blob-prop', 'data',
type_name='test-type',
content_type='application/test')
self.mock_http_client.put.assert_called_once_with(
'/artifacts/checked_name/test-id/blob-prop',
data='data', headers={'Content-Type': 'application/test'})
exp_headers = {
'Content-Type': 'application/octet-stream'
}
def test_get_type_list(self):
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,
exp_headers,
'data')]
self.assertEqual(expect, self.api.calls)
def test_upload_blob_custom_content_type(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
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_get_type_schema(self):
test_schema = {'schemas': {'checked_name': 'test-schema'}}
self.mock_http_client.get.return_value = (None, test_schema)
self.assertEqual('test-schema',
self.c.get_type_schema(type_name='test-type'))
self.mock_http_client.get.assert_called_once_with(
'/schemas/checked_name')
def test_add_external_location(self):
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a8'
data = self.controller.add_external_location(art_id,
'image',
'http://fake_url',
type_name='images')
expect_call = [
('PUT',
'/artifacts/images/3a4560a1-e585-443e-9b39-553b46ec92a8/image',
{'Content-Type': 'application/vnd+openstack.'
'glare-custom-location+json'},
'http://fake_url')]
self.assertEqual(expect_call, self.api.calls)
self.assertIsNone(data)
data = {
'url': 'http://fake_url',
'md5': '7CA772EE98D5CAF99F3674085D5E4124',
'sha1': None,
'sha256': None},
resp = self.c.add_external_location(
art_id, 'image',
data=data,
type_name='images')
self.c.http_client.put.assert_called_once_with(
'/artifacts/checked_name/'
'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):
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')
expect_call = [
('GET', '/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
{}, None),
('PATCH',
'/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
{'Content-Type': 'application/json-patch+json'},
[{'op': 'add',
'path': '/tags',
'value': ['a', 'b', 'c', '123']}])]
self.assertEqual(expect_call, self.api.calls)
self.c.http_client.get.assert_called_once_with(
'/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c')
self.c.http_client.patch.assert_called_once_with(
'/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
headers={'Content-Type': 'application/json-patch+json'},
json=[{'path': '/tags',
'value': ['a', 'b', 'c', '123'],
'op': 'add'}])
self.assertIsNotNone(data)
def test_add_existing_tag(self):
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')
expect_call = [
('GET', '/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
{}, None)]
self.assertEqual(expect_call, self.api.calls)
self.c.http_client.get.assert_called_once_with(
'/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c')
self.assertEqual(0, self.c.http_client.patch.call_count)
self.assertIsNotNone(data)
def test_remove_tag(self):
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')
expect_call = [
('GET', '/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
{}, None),
('PATCH',
'/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
{'Content-Type': 'application/json-patch+json'},
[{'op': 'add',
'path': '/tags',
'value': ['b', 'c']}])]
self.assertEqual(expect_call, self.api.calls)
self.c.http_client.get.assert_called_once_with(
'/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c')
self.c.http_client.patch.assert_called_once_with(
'/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
headers={'Content-Type': 'application/json-patch+json'},
json=[{'path': '/tags',
'value': ['b', 'c'],
'op': 'add'}])
self.assertIsNotNone(data)
def test_remove_nonexisting_tag(self):
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')
expect_call = [
('GET', '/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
{}, None)]
self.assertEqual(expect_call, self.api.calls)
self.c.http_client.get.assert_called_once_with(
'/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c')
self.assertEqual(0, self.c.http_client.patch.call_count)
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
# under the License.
from glareclient.common import utils
from glareclient import exc
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
import six
from six.moves.urllib import parse
from glareclient.common import utils
from glareclient import exc
class Controller(object):
def __init__(self, http_client, type_name=None):
@ -59,7 +61,7 @@ class Controller(object):
type_name = self._check_type_name(type_name)
kwargs.update({'name': name, 'version': version})
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
def update(self, artifact_id, type_name=None, remove_props=None,
@ -90,7 +92,7 @@ class Controller(object):
for prop_name in kwargs:
changes.append({'op': 'add', 'path': '/%s' % 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
def get(self, artifact_id, type_name=None):
@ -226,7 +228,7 @@ class Controller(object):
type_name = self._check_type_name(type_name)
hdrs = {'Content-Type': content_type}
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,
type_name=None):
@ -240,6 +242,10 @@ class Controller(object):
type_name = self._check_type_name(type_name)
hdrs = {'Content-Type': content_type}
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)
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)
url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
resp, body = self.http_client.get(url)
checksum = resp.headers.get('content-md5', None)
content_length = int(resp.headers.get('content-length', 0))
if checksum is not None and do_checksum:
body = utils.integrity_iter(body, checksum)
return utils.IterableWithLength(body, content_length)
resp, body = self.http_client.get(url, redirect=False,
stream=True,
headers={"Accept": "*/*"})
return utils.ResponseBlobWrapper(resp, do_checksum)
def get_type_list(self):
"""Get list of type names."""