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:
parent
7c62625b2a
commit
5e85f0fc3c
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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"]
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
@ -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."""
|
||||
|
Loading…
Reference in New Issue
Block a user