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:
committed by
Mike Fedosin
parent
7c62625b2a
commit
5e85f0fc3c
@@ -26,7 +26,6 @@ import requests
|
|||||||
import six
|
import six
|
||||||
from six.moves import urllib
|
from six.moves import urllib
|
||||||
|
|
||||||
from glareclient._i18n import _
|
|
||||||
from glareclient.common import exceptions as exc
|
from glareclient.common import exceptions as exc
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@@ -52,43 +51,6 @@ def get_system_ca_file():
|
|||||||
LOG.warning("System ca file could not be found.")
|
LOG.warning("System ca file could not be found.")
|
||||||
|
|
||||||
|
|
||||||
def _chunk_body(body):
|
|
||||||
chunk = body
|
|
||||||
while chunk:
|
|
||||||
chunk = body.read(CHUNKSIZE)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
yield chunk
|
|
||||||
|
|
||||||
|
|
||||||
def _set_request_params(kwargs_params):
|
|
||||||
"""Handle the common parameters used to send the request."""
|
|
||||||
|
|
||||||
data = kwargs_params.pop('data', None)
|
|
||||||
params = copy.deepcopy(kwargs_params)
|
|
||||||
headers = params.get('headers', {})
|
|
||||||
content_type = headers.get('Content-Type')
|
|
||||||
stream = params.get("stream", False)
|
|
||||||
|
|
||||||
if stream:
|
|
||||||
if data is not None:
|
|
||||||
data = _chunk_body(data)
|
|
||||||
content_type = content_type or 'application/octet-stream'
|
|
||||||
elif data is not None and not isinstance(data, six.string_types):
|
|
||||||
try:
|
|
||||||
data = jsonutils.dumps(data)
|
|
||||||
except TypeError:
|
|
||||||
raise exc.HTTPBadRequest("json is malformed.")
|
|
||||||
|
|
||||||
params['data'] = data
|
|
||||||
headers.update(
|
|
||||||
{'Content-Type': content_type or 'application/json'})
|
|
||||||
params['headers'] = headers
|
|
||||||
params['stream'] = stream
|
|
||||||
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_response(resp):
|
def _handle_response(resp):
|
||||||
content_type = resp.headers.get('Content-Type')
|
content_type = resp.headers.get('Content-Type')
|
||||||
if not content_type:
|
if not content_type:
|
||||||
@@ -107,17 +69,6 @@ def _handle_response(resp):
|
|||||||
return resp, body_iter
|
return resp, body_iter
|
||||||
|
|
||||||
|
|
||||||
def _close_after_stream(response, chunk_size):
|
|
||||||
"""Iterate over the content and ensure the response is closed after."""
|
|
||||||
# Yield each chunk in the response body
|
|
||||||
for chunk in response.iter_content(chunk_size=chunk_size):
|
|
||||||
yield chunk
|
|
||||||
# Once we're done streaming the body, ensure everything is closed.
|
|
||||||
# This will return the connection to the HTTPConnectionPool in urllib3
|
|
||||||
# and ideally reduce the number of HTTPConnectionPool full warnings.
|
|
||||||
response.close()
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPClient(object):
|
class HTTPClient(object):
|
||||||
|
|
||||||
def __init__(self, endpoint, **kwargs):
|
def __init__(self, endpoint, **kwargs):
|
||||||
@@ -305,8 +256,7 @@ class HTTPClient(object):
|
|||||||
return creds
|
return creds
|
||||||
|
|
||||||
def json_request(self, url, method, **kwargs):
|
def json_request(self, url, method, **kwargs):
|
||||||
params = _set_request_params(kwargs)
|
resp = self.request(url, method, **kwargs)
|
||||||
resp = self.request(url, method, **params)
|
|
||||||
return _handle_response(resp)
|
return _handle_response(resp)
|
||||||
|
|
||||||
def json_patch_request(self, url, method='PATCH', **kwargs):
|
def json_patch_request(self, url, method='PATCH', **kwargs):
|
||||||
@@ -336,37 +286,14 @@ class SessionClient(adapter.LegacyJsonAdapter):
|
|||||||
"""HTTP client based on Keystone client session."""
|
"""HTTP client based on Keystone client session."""
|
||||||
|
|
||||||
def request(self, url, method, **kwargs):
|
def request(self, url, method, **kwargs):
|
||||||
params = _set_request_params(kwargs)
|
|
||||||
redirect = kwargs.get('redirect')
|
|
||||||
|
|
||||||
resp, body = super(SessionClient, self).request(
|
resp, body = super(SessionClient, self).request(
|
||||||
url, method,
|
url, method, **kwargs)
|
||||||
**params)
|
|
||||||
|
|
||||||
if 400 <= resp.status_code < 600:
|
if resp.status_code == 300 or (400 <= resp.status_code < 600):
|
||||||
raise exc.from_response(resp)
|
|
||||||
elif resp.status_code in (301, 302, 305):
|
|
||||||
if redirect:
|
|
||||||
location = resp.headers.get('location')
|
|
||||||
path = self.strip_endpoint(location)
|
|
||||||
resp = self.request(path, method, **kwargs)
|
|
||||||
elif resp.status_code == 300:
|
|
||||||
raise exc.from_response(resp)
|
raise exc.from_response(resp)
|
||||||
|
|
||||||
if resp.headers.get('Content-Type') == 'application/octet-stream':
|
|
||||||
body = _close_after_stream(resp, CHUNKSIZE)
|
|
||||||
return resp, body
|
return resp, body
|
||||||
|
|
||||||
def strip_endpoint(self, location):
|
|
||||||
if location is None:
|
|
||||||
message = _("Location not returned with 302")
|
|
||||||
raise exc.InvalidEndpoint(message=message)
|
|
||||||
if (self.endpoint_override is not None and
|
|
||||||
location.lower().startswith(self.endpoint_override.lower())):
|
|
||||||
return location[len(self.endpoint_override):]
|
|
||||||
else:
|
|
||||||
return location
|
|
||||||
|
|
||||||
|
|
||||||
def construct_http_client(*args, **kwargs):
|
def construct_http_client(*args, **kwargs):
|
||||||
session = kwargs.pop('session', None)
|
session = kwargs.pop('session', None)
|
||||||
|
|||||||
@@ -13,9 +13,12 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import decimal
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import six
|
SPIN_CHARS = ('-', '\\', '|', '/')
|
||||||
|
CHUNKSIZE = 1024 * 64 # 64kB
|
||||||
|
|
||||||
|
|
||||||
class _ProgressBarBase(object):
|
class _ProgressBarBase(object):
|
||||||
@@ -30,21 +33,37 @@ class _ProgressBarBase(object):
|
|||||||
:note: The progress will be displayed only if sys.stdout is a tty.
|
:note: The progress will be displayed only if sys.stdout is a tty.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, wrapped, totalsize):
|
def __init__(self, wrapped):
|
||||||
self._wrapped = wrapped
|
self._wrapped = wrapped
|
||||||
self._totalsize = float(totalsize)
|
|
||||||
self._show_progress = sys.stdout.isatty()
|
self._show_progress = sys.stdout.isatty()
|
||||||
self._percent = 0
|
self._percent = 0
|
||||||
|
self._totalread = 0
|
||||||
|
self._spin_index = 0
|
||||||
|
if hasattr(wrapped, "len"):
|
||||||
|
self._totalsize = wrapped.len
|
||||||
|
elif hasattr(wrapped, "fileno"):
|
||||||
|
self._totalsize = os.fstat(wrapped.fileno()).st_size
|
||||||
|
else:
|
||||||
|
self._totalsize = 0
|
||||||
|
|
||||||
def _display_progress_bar(self, size_read):
|
def _display_progress_bar(self, size_read):
|
||||||
|
self._totalread += size_read
|
||||||
if self._show_progress:
|
if self._show_progress:
|
||||||
if self._totalsize == 0:
|
if self._totalsize:
|
||||||
self._totalsize = size_read
|
percent = float(self._totalread) / float(self._totalsize)
|
||||||
self._percent += size_read / self._totalsize
|
|
||||||
# Output something like this: [==========> ] 49%
|
# Output something like this: [==========> ] 49%
|
||||||
sys.stdout.write('\r[{0:<30}] {1:.0%}'.format(
|
sys.stdout.write('\r[{0:<30}] {1:.0%}'.format(
|
||||||
'=' * int(round(self._percent * 29)) + '>', self._percent
|
'=' * 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()
|
sys.stdout.flush()
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
@@ -70,24 +89,12 @@ class VerboseFileWrapper(_ProgressBarBase):
|
|||||||
sys.stdout.write('\n')
|
sys.stdout.write('\n')
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class VerboseIteratorWrapper(_ProgressBarBase):
|
|
||||||
"""An iterator wrapper with a progress bar.
|
|
||||||
|
|
||||||
The iterator wrapper shows and advances a progress bar whenever the
|
|
||||||
wrapped data is consumed from the iterator.
|
|
||||||
|
|
||||||
:note: Use only with iterator that yield strings.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
try:
|
try:
|
||||||
data = six.next(self._wrapped)
|
data = self._wrapped.next()
|
||||||
# NOTE(mouad): Assuming that data is a string b/c otherwise calling
|
|
||||||
# len function will not make any sense.
|
|
||||||
self._display_progress_bar(len(data))
|
self._display_progress_bar(len(data))
|
||||||
return data
|
return data
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from __future__ import print_function
|
|||||||
import errno
|
import errno
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import six
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
@@ -28,6 +27,7 @@ else:
|
|||||||
|
|
||||||
from oslo_utils import encodeutils
|
from oslo_utils import encodeutils
|
||||||
from oslo_utils import importutils
|
from oslo_utils import importutils
|
||||||
|
import requests
|
||||||
|
|
||||||
SENSITIVE_HEADERS = ('X-Auth-Token', )
|
SENSITIVE_HEADERS = ('X-Auth-Token', )
|
||||||
|
|
||||||
@@ -58,41 +58,40 @@ def exit(msg='', exit_code=1):
|
|||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
|
||||||
def integrity_iter(iter, checksum):
|
class ResponseBlobWrapper(object):
|
||||||
"""Check blob integrity.
|
"""Represent HTTP response as iterator with known length."""
|
||||||
|
|
||||||
:raises: IOError
|
def __init__(self, resp, verify_md5=True):
|
||||||
"""
|
self.hash_md5 = resp.headers.get("Content-MD5")
|
||||||
md5sum = hashlib.md5()
|
self.check_md5 = hashlib.md5()
|
||||||
for chunk in iter:
|
if 301 <= resp.status_code <= 302:
|
||||||
yield chunk
|
# NOTE(sskripnick): handle redirect manually to prevent sending
|
||||||
if isinstance(chunk, six.string_types):
|
# auth token to external resource.
|
||||||
chunk = six.b(chunk)
|
# Use stream=True to prevent reading whole response into memory.
|
||||||
md5sum.update(chunk)
|
# Set Accept-Encoding explicitly to "identity" because setting
|
||||||
md5sum = md5sum.hexdigest()
|
# stream=True forces Accept-Encoding to be "gzip, defalate".
|
||||||
if md5sum != checksum:
|
# It should be "identity" because we should know Content-Length.
|
||||||
raise IOError(errno.EPIPE,
|
resp = requests.get(resp.headers.get("Location"),
|
||||||
'Corrupt blob download. Checksum was %s expected %s' %
|
headers={"Accept-Encoding": "identity"})
|
||||||
(md5sum, checksum))
|
self.len = resp.headers.get("Content-Length", 0)
|
||||||
|
self.iter = resp.iter_content(65536)
|
||||||
|
|
||||||
class IterableWithLength(object):
|
|
||||||
def __init__(self, iterable, length):
|
|
||||||
self.iterable = iterable
|
|
||||||
self.length = length
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
try:
|
return self
|
||||||
for chunk in self.iterable:
|
|
||||||
yield chunk
|
|
||||||
finally:
|
|
||||||
self.iterable.close()
|
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
return next(self.iterable)
|
try:
|
||||||
|
data = self.iter.next()
|
||||||
|
self.check_md5.update(data)
|
||||||
|
return data
|
||||||
|
except StopIteration:
|
||||||
|
if self.check_md5.hexdigest() != self.hash_md5:
|
||||||
|
raise IOError(errno.EPIPE,
|
||||||
|
'Checksum mismatch: %s (expected %s)' %
|
||||||
|
(self.check_md5.hexdigest(), self.hash_md5))
|
||||||
|
raise
|
||||||
|
|
||||||
def __len__(self):
|
__next__ = next
|
||||||
return self.length
|
|
||||||
|
|
||||||
|
|
||||||
def get_item_properties(item, fields, mixed_case_fields=None, formatters=None):
|
def get_item_properties(item, fields, mixed_case_fields=None, formatters=None):
|
||||||
@@ -158,60 +157,3 @@ def save_blob(data, path):
|
|||||||
finally:
|
finally:
|
||||||
if path is not None:
|
if path is not None:
|
||||||
blob.close()
|
blob.close()
|
||||||
|
|
||||||
|
|
||||||
def get_data_file(blob):
|
|
||||||
if blob:
|
|
||||||
return open(blob, 'rb')
|
|
||||||
else:
|
|
||||||
# distinguish cases where:
|
|
||||||
# (1) stdin is not valid (as in cron jobs):
|
|
||||||
# glare ... <&-
|
|
||||||
# (2) blob is provided through standard input:
|
|
||||||
# glare ... < /tmp/file or cat /tmp/file | glare ...
|
|
||||||
# (3) no blob provided:
|
|
||||||
# glare ...
|
|
||||||
try:
|
|
||||||
os.fstat(0)
|
|
||||||
except OSError:
|
|
||||||
# (1) stdin is not valid (closed...)
|
|
||||||
return None
|
|
||||||
if not sys.stdin.isatty():
|
|
||||||
# (2) blob data is provided through standard input
|
|
||||||
blob_data = sys.stdin
|
|
||||||
if hasattr(sys.stdin, 'buffer'):
|
|
||||||
blob_data = sys.stdin.buffer
|
|
||||||
if msvcrt:
|
|
||||||
msvcrt.setmode(blob_data.fileno(), os.O_BINARY)
|
|
||||||
return blob_data
|
|
||||||
else:
|
|
||||||
# (3) no blob data provided
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_file_size(file_obj):
|
|
||||||
"""Analyze file-like object and attempt to determine its size.
|
|
||||||
|
|
||||||
:param file_obj: file-like object.
|
|
||||||
:retval The file's size or None if it cannot be determined.
|
|
||||||
"""
|
|
||||||
if (hasattr(file_obj, 'seek') and hasattr(file_obj, 'tell') and
|
|
||||||
(six.PY2 or six.PY3 and file_obj.seekable())):
|
|
||||||
try:
|
|
||||||
curr = file_obj.tell()
|
|
||||||
file_obj.seek(0, os.SEEK_END)
|
|
||||||
size = file_obj.tell()
|
|
||||||
file_obj.seek(curr)
|
|
||||||
return size
|
|
||||||
except IOError as e:
|
|
||||||
if e.errno == errno.ESPIPE:
|
|
||||||
# Illegal seek. This means the file object
|
|
||||||
# is a pipe (e.g. the user is trying
|
|
||||||
# to pipe blob to the client,
|
|
||||||
# echo testdata | bin/glare add blah...), or
|
|
||||||
# that file object is empty, or that a file-like
|
|
||||||
# object which doesn't support 'seek/tell' has
|
|
||||||
# been supplied.
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from osc_lib.command import command
|
|||||||
|
|
||||||
from glareclient.common import progressbar
|
from glareclient.common import progressbar
|
||||||
from glareclient.common import utils
|
from glareclient.common import utils
|
||||||
|
from glareclient import exc
|
||||||
from glareclient.osc.v1 import TypeMapperAction
|
from glareclient.osc.v1 import TypeMapperAction
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@@ -107,11 +108,15 @@ class UploadBlob(command.ShowOne):
|
|||||||
parsed_args.blob_property = _default_blob_property(
|
parsed_args.blob_property = _default_blob_property(
|
||||||
parsed_args.type_name)
|
parsed_args.type_name)
|
||||||
|
|
||||||
blob = utils.get_data_file(parsed_args.file)
|
if parsed_args.file is None:
|
||||||
|
if sys.stdin.isatty():
|
||||||
|
raise exc.CommandError('Blob file should be specified or '
|
||||||
|
'explicitly connected to stdin')
|
||||||
|
blob = sys.stdin
|
||||||
|
else:
|
||||||
|
blob = open(parsed_args.file, 'rb')
|
||||||
if parsed_args.progress:
|
if parsed_args.progress:
|
||||||
file_size = utils.get_file_size(blob)
|
blob = progressbar.VerboseFileWrapper(blob)
|
||||||
if file_size is not None:
|
|
||||||
blob = progressbar.VerboseFileWrapper(blob, file_size)
|
|
||||||
|
|
||||||
client.artifacts.upload_blob(af_id, parsed_args.blob_property, blob,
|
client.artifacts.upload_blob(af_id, parsed_args.blob_property, blob,
|
||||||
content_type=parsed_args.content_type,
|
content_type=parsed_args.content_type,
|
||||||
@@ -186,7 +191,7 @@ class DownloadBlob(command.Command):
|
|||||||
parsed_args.blob_property,
|
parsed_args.blob_property,
|
||||||
type_name=parsed_args.type_name)
|
type_name=parsed_args.type_name)
|
||||||
if parsed_args.progress:
|
if parsed_args.progress:
|
||||||
data = progressbar.VerboseIteratorWrapper(data, len(data))
|
data = progressbar.VerboseFileWrapper(data)
|
||||||
if not (sys.stdout.isatty() and parsed_args.file is None):
|
if not (sys.stdout.isatty() and parsed_args.file is None):
|
||||||
utils.save_blob(data, parsed_args.file)
|
utils.save_blob(data, parsed_args.file)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -14,12 +14,108 @@
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
from glareclient import exc
|
||||||
from glareclient.osc.v1 import blobs as osc_blob
|
from glareclient.osc.v1 import blobs as osc_blob
|
||||||
from glareclient.tests.unit.osc.v1 import fakes
|
from glareclient.tests.unit.osc.v1 import fakes
|
||||||
from glareclient.v1 import artifacts as api_art
|
from glareclient.v1 import artifacts as api_art
|
||||||
import testtools
|
import testtools
|
||||||
|
|
||||||
|
|
||||||
|
class TestUpload(testtools.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.mock_app = mock.Mock()
|
||||||
|
self.mock_args = mock.Mock()
|
||||||
|
self.mock_manager = mock.Mock()
|
||||||
|
self.mock_manager.artifacts.get.return_value = {'image': {}}
|
||||||
|
super(TestUpload, self).setUp()
|
||||||
|
|
||||||
|
@mock.patch('glareclient.osc.v1.blobs.progressbar')
|
||||||
|
@mock.patch('glareclient.osc.v1.blobs.sys')
|
||||||
|
@mock.patch('glareclient.osc.v1.blobs.open', create=True)
|
||||||
|
@mock.patch('glareclient.osc.v1.blobs.get_artifact_id')
|
||||||
|
def test_upload_file_progress(self, mock_get_id,
|
||||||
|
mock_open, mock_sys, mock_progressbar):
|
||||||
|
mock_parsed_args = mock.Mock(name='test-id',
|
||||||
|
id=True,
|
||||||
|
blob_property='image',
|
||||||
|
file='/path/file',
|
||||||
|
progress=True,
|
||||||
|
content_type='application/test',
|
||||||
|
type_name='test-type')
|
||||||
|
mock_get_id.return_value = 'test-id'
|
||||||
|
cli = osc_blob.UploadBlob(self.mock_app, self.mock_args)
|
||||||
|
cli.app.client_manager.artifact = self.mock_manager
|
||||||
|
cli.dict2columns = mock.Mock(return_value=42)
|
||||||
|
self.assertEqual(42, cli.take_action(mock_parsed_args))
|
||||||
|
cli.dict2columns.assert_called_once_with({'blob_property': 'image'})
|
||||||
|
upload_args = ['test-id', 'image',
|
||||||
|
mock_progressbar.VerboseFileWrapper.return_value]
|
||||||
|
upload_kwargs = {'content_type': 'application/test',
|
||||||
|
'type_name': 'test-type'}
|
||||||
|
self.mock_manager.artifacts.upload_blob.\
|
||||||
|
assert_called_once_with(*upload_args, **upload_kwargs)
|
||||||
|
|
||||||
|
@mock.patch('glareclient.osc.v1.blobs.sys')
|
||||||
|
@mock.patch('glareclient.osc.v1.blobs.open', create=True)
|
||||||
|
@mock.patch('glareclient.osc.v1.blobs.get_artifact_id')
|
||||||
|
def test_upload_file_no_progress(self, mock_get_id, mock_open, mock_sys):
|
||||||
|
mock_parsed_args = mock.Mock(name='test-id',
|
||||||
|
id=True,
|
||||||
|
blob_property='image',
|
||||||
|
progress=False,
|
||||||
|
file='/path/file',
|
||||||
|
content_type='application/test',
|
||||||
|
type_name='test-type')
|
||||||
|
mock_get_id.return_value = 'test-id'
|
||||||
|
cli = osc_blob.UploadBlob(self.mock_app, self.mock_args)
|
||||||
|
cli.app.client_manager.artifact = self.mock_manager
|
||||||
|
cli.dict2columns = mock.Mock(return_value=42)
|
||||||
|
self.assertEqual(42, cli.take_action(mock_parsed_args))
|
||||||
|
cli.dict2columns.assert_called_once_with({'blob_property': 'image'})
|
||||||
|
upload_args = ['test-id', 'image', mock_open.return_value]
|
||||||
|
upload_kwargs = {'content_type': 'application/test',
|
||||||
|
'type_name': 'test-type'}
|
||||||
|
self.mock_manager.artifacts.upload_blob.\
|
||||||
|
assert_called_once_with(*upload_args, **upload_kwargs)
|
||||||
|
|
||||||
|
@mock.patch('glareclient.osc.v1.blobs.sys')
|
||||||
|
@mock.patch('glareclient.osc.v1.blobs.get_artifact_id')
|
||||||
|
def test_upload_file_stdin(self, mock_get_id, mock_sys):
|
||||||
|
mock_sys.stdin.isatty.return_value = False
|
||||||
|
mock_parsed_args = mock.Mock(name='test-id',
|
||||||
|
id=True,
|
||||||
|
blob_property='image',
|
||||||
|
progress=False,
|
||||||
|
file=None,
|
||||||
|
content_type='application/test',
|
||||||
|
type_name='test-type')
|
||||||
|
mock_get_id.return_value = 'test-id'
|
||||||
|
cli = osc_blob.UploadBlob(self.mock_app, self.mock_args)
|
||||||
|
cli.app.client_manager.artifact = self.mock_manager
|
||||||
|
cli.dict2columns = mock.Mock(return_value=42)
|
||||||
|
self.assertEqual(42, cli.take_action(mock_parsed_args))
|
||||||
|
cli.dict2columns.assert_called_once_with({'blob_property': 'image'})
|
||||||
|
upload_args = ['test-id', 'image', mock_sys.stdin]
|
||||||
|
upload_kwargs = {'content_type': 'application/test',
|
||||||
|
'type_name': 'test-type'}
|
||||||
|
self.mock_manager.artifacts.upload_blob.\
|
||||||
|
assert_called_once_with(*upload_args, **upload_kwargs)
|
||||||
|
|
||||||
|
@mock.patch('glareclient.osc.v1.blobs.sys')
|
||||||
|
def test_upload_file_stdin_isatty(self, mock_sys):
|
||||||
|
mock_sys.stdin.isatty.return_value = True
|
||||||
|
mock_parsed_args = mock.Mock(id='test-id',
|
||||||
|
blob_property='image',
|
||||||
|
progress=False,
|
||||||
|
file=None,
|
||||||
|
content_type='application/test',
|
||||||
|
type_name='test-type')
|
||||||
|
cli = osc_blob.UploadBlob(self.mock_app, self.mock_args)
|
||||||
|
cli.app.client_manager.artifact = self.mock_manager
|
||||||
|
self.assertRaises(exc.CommandError, cli.take_action, mock_parsed_args)
|
||||||
|
|
||||||
|
|
||||||
class TestBlobs(fakes.TestArtifacts):
|
class TestBlobs(fakes.TestArtifacts):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestBlobs, self).setUp()
|
super(TestBlobs, self).setUp()
|
||||||
@@ -28,187 +124,6 @@ class TestBlobs(fakes.TestArtifacts):
|
|||||||
self.http = mock.MagicMock()
|
self.http = mock.MagicMock()
|
||||||
|
|
||||||
|
|
||||||
class TestUploadBlob(TestBlobs):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestUploadBlob, self).setUp()
|
|
||||||
self.blob_mock.call.return_value = \
|
|
||||||
api_art.Controller(self.http, type_name='images')
|
|
||||||
|
|
||||||
# Command to test
|
|
||||||
self.cmd = osc_blob.UploadBlob(self.app, None)
|
|
||||||
|
|
||||||
self.COLUMNS = ('blob_property', 'content_type', 'external',
|
|
||||||
'md5', 'sha1', 'sha256', 'size', 'status', 'url')
|
|
||||||
|
|
||||||
def test_upload_images(self):
|
|
||||||
exp_data = ('image', 'application/octet-stream', False,
|
|
||||||
'35d83e8eedfbdb87ff97d1f2761f8ebf',
|
|
||||||
'942854360eeec1335537702399c5aed940401602',
|
|
||||||
'd8a7834fc6652f316322d80196f6dcf2'
|
|
||||||
'94417030e37c15412e4deb7a67a367dd',
|
|
||||||
594, 'active', 'fake_url')
|
|
||||||
arglist = ['images',
|
|
||||||
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
|
|
||||||
'--file', '/path/to/file']
|
|
||||||
verify = [('type_name', 'images')]
|
|
||||||
|
|
||||||
parsed_args = self.check_parser(self.cmd, arglist, verify)
|
|
||||||
columns, data = self.cmd.take_action(parsed_args)
|
|
||||||
self.assertEqual(self.COLUMNS, columns)
|
|
||||||
self.assertEqual(exp_data, data)
|
|
||||||
|
|
||||||
def test_upload_tosca_template(self):
|
|
||||||
exp_data = ('template', 'application/octet-stream', False,
|
|
||||||
'35d83e8eedfbdb87ff97d1f2761f8ebf',
|
|
||||||
'942854360eeec1335537702399c5aed940401602',
|
|
||||||
'd8a7834fc6652f316322d80196f6dcf2'
|
|
||||||
'94417030e37c15412e4deb7a67a367dd',
|
|
||||||
594, 'active', 'fake_url')
|
|
||||||
arglist = ['tosca_templates',
|
|
||||||
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
|
|
||||||
'--file', '/path/to/file']
|
|
||||||
verify = [('type_name', 'tosca_templates')]
|
|
||||||
|
|
||||||
parsed_args = self.check_parser(self.cmd, arglist, verify)
|
|
||||||
columns, data = self.cmd.take_action(parsed_args)
|
|
||||||
self.assertEqual(self.COLUMNS, columns)
|
|
||||||
self.assertEqual(exp_data, data)
|
|
||||||
|
|
||||||
def test_upload_heat_template(self):
|
|
||||||
exp_data = ('template', 'application/octet-stream', False,
|
|
||||||
'35d83e8eedfbdb87ff97d1f2761f8ebf',
|
|
||||||
'942854360eeec1335537702399c5aed940401602',
|
|
||||||
'd8a7834fc6652f316322d80196f6dcf2'
|
|
||||||
'94417030e37c15412e4deb7a67a367dd',
|
|
||||||
594, 'active', 'fake_url')
|
|
||||||
arglist = ['heat_templates',
|
|
||||||
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
|
|
||||||
'--file', '/path/to/file']
|
|
||||||
verify = [('type_name', 'heat_templates')]
|
|
||||||
|
|
||||||
parsed_args = self.check_parser(self.cmd, arglist, verify)
|
|
||||||
columns, data = self.cmd.take_action(parsed_args)
|
|
||||||
self.assertEqual(self.COLUMNS, columns)
|
|
||||||
self.assertEqual(exp_data, data)
|
|
||||||
|
|
||||||
def test_upload_environment(self):
|
|
||||||
exp_data = ('environment', 'application/octet-stream', False,
|
|
||||||
'35d83e8eedfbdb87ff97d1f2761f8ebf',
|
|
||||||
'942854360eeec1335537702399c5aed940401602',
|
|
||||||
'd8a7834fc6652f316322d80196f6dcf2'
|
|
||||||
'94417030e37c15412e4deb7a67a367dd',
|
|
||||||
594, 'active', 'fake_url')
|
|
||||||
arglist = ['heat_environments',
|
|
||||||
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
|
|
||||||
'--file', '/path/to/file']
|
|
||||||
verify = [('type_name', 'heat_environments')]
|
|
||||||
|
|
||||||
parsed_args = self.check_parser(self.cmd, arglist, verify)
|
|
||||||
columns, data = self.cmd.take_action(parsed_args)
|
|
||||||
self.assertEqual(self.COLUMNS, columns)
|
|
||||||
self.assertEqual(exp_data, data)
|
|
||||||
|
|
||||||
def test_upload_package(self):
|
|
||||||
exp_data = ('package', 'application/octet-stream', False,
|
|
||||||
'35d83e8eedfbdb87ff97d1f2761f8ebf',
|
|
||||||
'942854360eeec1335537702399c5aed940401602',
|
|
||||||
'd8a7834fc6652f316322d80196f6dcf2'
|
|
||||||
'94417030e37c15412e4deb7a67a367dd',
|
|
||||||
594, 'active', 'fake_url')
|
|
||||||
arglist = ['murano_packages',
|
|
||||||
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
|
|
||||||
'--file', '/path/to/file']
|
|
||||||
verify = [('type_name', 'murano_packages')]
|
|
||||||
|
|
||||||
parsed_args = self.check_parser(self.cmd, arglist, verify)
|
|
||||||
columns, data = self.cmd.take_action(parsed_args)
|
|
||||||
self.assertEqual(self.COLUMNS, columns)
|
|
||||||
self.assertEqual(exp_data, data)
|
|
||||||
|
|
||||||
def test_upload_bad(self):
|
|
||||||
arglist = ['user_type',
|
|
||||||
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
|
|
||||||
'--file', '/path/to/file']
|
|
||||||
verify = [('type_name', 'user_type')]
|
|
||||||
parsed_args = self.check_parser(self.cmd, arglist, verify)
|
|
||||||
with testtools.ExpectedException(SystemExit):
|
|
||||||
self.cmd.take_action(parsed_args)
|
|
||||||
|
|
||||||
def test_upload_with_custom_content_type(self):
|
|
||||||
exp_data = ('template', 'application/x-yaml', False,
|
|
||||||
'35d83e8eedfbdb87ff97d1f2761f8ebf',
|
|
||||||
'942854360eeec1335537702399c5aed940401602',
|
|
||||||
'd8a7834fc6652f316322d80196f6dcf2'
|
|
||||||
'94417030e37c15412e4deb7a67a367dd',
|
|
||||||
594, 'active', 'fake_url')
|
|
||||||
|
|
||||||
mocked_get = {
|
|
||||||
"status": "active",
|
|
||||||
"url": "fake_url",
|
|
||||||
"md5": "35d83e8eedfbdb87ff97d1f2761f8ebf",
|
|
||||||
"sha1": "942854360eeec1335537702399c5aed940401602",
|
|
||||||
"sha256": "d8a7834fc6652f316322d80196f6dcf2"
|
|
||||||
"94417030e37c15412e4deb7a67a367dd",
|
|
||||||
"external": False,
|
|
||||||
"content_type": "application/x-yaml",
|
|
||||||
"size": 594}
|
|
||||||
self.app.client_manager.artifact.artifacts.get = \
|
|
||||||
lambda id, type_name: {'template': mocked_get}
|
|
||||||
|
|
||||||
arglist = ['tosca_templates',
|
|
||||||
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
|
|
||||||
'--file', '/path/to/file',
|
|
||||||
'--content-type', 'application/x-yaml']
|
|
||||||
verify = [('type_name', 'tosca_templates')]
|
|
||||||
|
|
||||||
parsed_args = self.check_parser(self.cmd, arglist, verify)
|
|
||||||
columns, data = self.cmd.take_action(parsed_args)
|
|
||||||
self.assertEqual(self.COLUMNS, columns)
|
|
||||||
self.assertEqual(exp_data, data)
|
|
||||||
|
|
||||||
def test_upload_blob_with_blob_prop(self):
|
|
||||||
exp_data = ('blob', 'application/octet-stream', False,
|
|
||||||
'35d83e8eedfbdb87ff97d1f2761f8ebf',
|
|
||||||
'942854360eeec1335537702399c5aed940401602',
|
|
||||||
'd8a7834fc6652f316322d80196f6dcf2'
|
|
||||||
'94417030e37c15412e4deb7a67a367dd',
|
|
||||||
594, 'active', 'fake_url')
|
|
||||||
arglist = ['images',
|
|
||||||
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
|
|
||||||
'--file', '/path/to/file',
|
|
||||||
'--blob-property', 'blob']
|
|
||||||
verify = [('type_name', 'images')]
|
|
||||||
|
|
||||||
parsed_args = self.check_parser(self.cmd, arglist, verify)
|
|
||||||
columns, data = self.cmd.take_action(parsed_args)
|
|
||||||
self.assertEqual(self.COLUMNS, columns)
|
|
||||||
self.assertEqual(exp_data, data)
|
|
||||||
|
|
||||||
def test_upload_blob_dict(self):
|
|
||||||
exp_data = ('nested_templates/blob', 'application/octet-stream',
|
|
||||||
False,
|
|
||||||
'35d83e8eedfbdb87ff97d1f2761f8ebf',
|
|
||||||
'942854360eeec1335537702399c5aed940401602',
|
|
||||||
'd8a7834fc6652f316322d80196f6dcf2'
|
|
||||||
'94417030e37c15412e4deb7a67a367dd',
|
|
||||||
594, 'active', 'fake_url')
|
|
||||||
arglist = ['images',
|
|
||||||
'fc15c365-d4f9-4b8b-a090-d9e230f1f6ba',
|
|
||||||
'--file', '/path/to/file',
|
|
||||||
'--blob-property', 'nested_templates/blob']
|
|
||||||
verify = [('type_name', 'images')]
|
|
||||||
|
|
||||||
parsed_args = self.check_parser(self.cmd, arglist, verify)
|
|
||||||
self.app.client_manager.artifact.artifacts.get = \
|
|
||||||
lambda *args, **kwargs: {
|
|
||||||
'nested_templates': {'blob': fakes.blob_fixture}
|
|
||||||
}
|
|
||||||
columns, data = self.cmd.take_action(parsed_args)
|
|
||||||
self.app.client_manager.artifact.artifacts.get = fakes.mock_get
|
|
||||||
self.assertEqual(self.COLUMNS, columns)
|
|
||||||
self.assertEqual(exp_data, data)
|
|
||||||
|
|
||||||
|
|
||||||
class TestDownloadBlob(TestBlobs):
|
class TestDownloadBlob(TestBlobs):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestDownloadBlob, self).setUp()
|
super(TestDownloadBlob, self).setUp()
|
||||||
|
|||||||
@@ -119,10 +119,7 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
mock_request.assert_called_once_with(
|
mock_request.assert_called_once_with(
|
||||||
'GET', 'http://example.com:9494',
|
'GET', 'http://example.com:9494',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'})
|
||||||
data=None,
|
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'})
|
|
||||||
|
|
||||||
def test_http_json_request_argument_passed_to_requests(self, mock_request):
|
def test_http_json_request_argument_passed_to_requests(self, mock_request):
|
||||||
"""Check that we have sent the proper arguments to requests."""
|
"""Check that we have sent the proper arguments to requests."""
|
||||||
@@ -148,9 +145,7 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
cert=('RANDOM_CERT_FILE', 'RANDOM_KEY_FILE'),
|
cert=('RANDOM_CERT_FILE', 'RANDOM_KEY_FILE'),
|
||||||
verify=True,
|
verify=True,
|
||||||
data='text',
|
data='text',
|
||||||
stream=False,
|
headers={'X-Auth-Url': 'http://AUTH_URL',
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'X-Auth-Url': 'http://AUTH_URL',
|
|
||||||
'User-Agent': 'python-glareclient'})
|
'User-Agent': 'python-glareclient'})
|
||||||
|
|
||||||
def test_http_json_request_w_req_body(self, mock_request):
|
def test_http_json_request_w_req_body(self, mock_request):
|
||||||
@@ -169,9 +164,7 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
'GET', 'http://example.com:9494',
|
'GET', 'http://example.com:9494',
|
||||||
data='test-body',
|
data='test-body',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'})
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'})
|
|
||||||
|
|
||||||
def test_http_json_request_non_json_resp_cont_type(self, mock_request):
|
def test_http_json_request_non_json_resp_cont_type(self, mock_request):
|
||||||
# Record a 200
|
# Record a 200
|
||||||
@@ -187,9 +180,7 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
mock_request.assert_called_once_with(
|
mock_request.assert_called_once_with(
|
||||||
'GET', 'http://example.com:9494', data='test-data',
|
'GET', 'http://example.com:9494', data='test-data',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'})
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'})
|
|
||||||
|
|
||||||
def test_http_json_request_invalid_json(self, mock_request):
|
def test_http_json_request_invalid_json(self, mock_request):
|
||||||
# Record a 200
|
# Record a 200
|
||||||
@@ -206,10 +197,7 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
mock_request.assert_called_once_with(
|
mock_request.assert_called_once_with(
|
||||||
'GET', 'http://example.com:9494',
|
'GET', 'http://example.com:9494',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'})
|
||||||
data=None,
|
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'})
|
|
||||||
|
|
||||||
def test_http_manual_redirect_delete(self, mock_request):
|
def test_http_manual_redirect_delete(self, mock_request):
|
||||||
mock_request.side_effect = [
|
mock_request.side_effect = [
|
||||||
@@ -229,16 +217,10 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
mock_request.assert_has_calls([
|
mock_request.assert_has_calls([
|
||||||
mock.call('DELETE', 'http://example.com:9494/foo',
|
mock.call('DELETE', 'http://example.com:9494/foo',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'}),
|
||||||
data=None,
|
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'}),
|
|
||||||
mock.call('DELETE', 'http://example.com:9494/foo/bar',
|
mock.call('DELETE', 'http://example.com:9494/foo/bar',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'})
|
||||||
data=None,
|
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'})
|
|
||||||
])
|
])
|
||||||
|
|
||||||
def test_http_manual_redirect_post(self, mock_request):
|
def test_http_manual_redirect_post(self, mock_request):
|
||||||
@@ -253,22 +235,18 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
'{}')]
|
'{}')]
|
||||||
|
|
||||||
client = http.HTTPClient('http://example.com:9494/foo')
|
client = http.HTTPClient('http://example.com:9494/foo')
|
||||||
resp, body = client.json_request('', 'POST')
|
resp, body = client.json_request('', 'POST', json={})
|
||||||
|
|
||||||
self.assertEqual(200, resp.status_code)
|
self.assertEqual(200, resp.status_code)
|
||||||
mock_request.assert_has_calls([
|
mock_request.assert_has_calls([
|
||||||
mock.call('POST', 'http://example.com:9494/foo',
|
mock.call('POST', 'http://example.com:9494/foo',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'},
|
||||||
data=None,
|
json={}),
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'}),
|
|
||||||
mock.call('POST', 'http://example.com:9494/foo/bar',
|
mock.call('POST', 'http://example.com:9494/foo/bar',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'},
|
||||||
data=None,
|
json={})
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'})
|
|
||||||
])
|
])
|
||||||
|
|
||||||
def test_http_manual_redirect_put(self, mock_request):
|
def test_http_manual_redirect_put(self, mock_request):
|
||||||
@@ -283,22 +261,18 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
'{}')]
|
'{}')]
|
||||||
|
|
||||||
client = http.HTTPClient('http://example.com:9494/foo')
|
client = http.HTTPClient('http://example.com:9494/foo')
|
||||||
resp, body = client.json_request('', 'PUT')
|
resp, body = client.json_request('', 'PUT', json={})
|
||||||
|
|
||||||
self.assertEqual(200, resp.status_code)
|
self.assertEqual(200, resp.status_code)
|
||||||
mock_request.assert_has_calls([
|
mock_request.assert_has_calls([
|
||||||
mock.call('PUT', 'http://example.com:9494/foo',
|
mock.call('PUT', 'http://example.com:9494/foo',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'},
|
||||||
data=None,
|
json={}),
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'}),
|
|
||||||
mock.call('PUT', 'http://example.com:9494/foo/bar',
|
mock.call('PUT', 'http://example.com:9494/foo/bar',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'},
|
||||||
data=None,
|
json={})
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'})
|
|
||||||
])
|
])
|
||||||
|
|
||||||
def test_http_manual_redirect_prohibited(self, mock_request):
|
def test_http_manual_redirect_prohibited(self, mock_request):
|
||||||
@@ -313,10 +287,7 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
mock_request.assert_called_once_with(
|
mock_request.assert_called_once_with(
|
||||||
'DELETE', 'http://example.com:9494/foo',
|
'DELETE', 'http://example.com:9494/foo',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'})
|
||||||
data=None,
|
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'})
|
|
||||||
|
|
||||||
def test_http_manual_redirect_error_without_location(self, mock_request):
|
def test_http_manual_redirect_error_without_location(self, mock_request):
|
||||||
mock_request.return_value = \
|
mock_request.return_value = \
|
||||||
@@ -330,10 +301,7 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
mock_request.assert_called_once_with(
|
mock_request.assert_called_once_with(
|
||||||
'DELETE', 'http://example.com:9494/foo',
|
'DELETE', 'http://example.com:9494/foo',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'})
|
||||||
data=None,
|
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'})
|
|
||||||
|
|
||||||
def test_http_json_request_redirect(self, mock_request):
|
def test_http_json_request_redirect(self, mock_request):
|
||||||
# Record the 302
|
# Record the 302
|
||||||
@@ -344,7 +312,7 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
''),
|
''),
|
||||||
fakes.FakeHTTPResponse(
|
fakes.FakeHTTPResponse(
|
||||||
200, 'OK',
|
200, 'OK',
|
||||||
{'content-type': 'application/json'},
|
{},
|
||||||
'{}')]
|
'{}')]
|
||||||
|
|
||||||
client = http.HTTPClient('http://example.com:9494')
|
client = http.HTTPClient('http://example.com:9494')
|
||||||
@@ -355,16 +323,10 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
mock_request.assert_has_calls([
|
mock_request.assert_has_calls([
|
||||||
mock.call('GET', 'http://example.com:9494',
|
mock.call('GET', 'http://example.com:9494',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'}),
|
||||||
data=None,
|
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'}),
|
|
||||||
mock.call('GET', 'http://example.com:9494',
|
mock.call('GET', 'http://example.com:9494',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'})
|
||||||
data=None,
|
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'})
|
|
||||||
])
|
])
|
||||||
|
|
||||||
def test_http_404_json_request(self, mock_request):
|
def test_http_404_json_request(self, mock_request):
|
||||||
@@ -381,10 +343,7 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
mock_request.assert_called_once_with(
|
mock_request.assert_called_once_with(
|
||||||
'GET', 'http://example.com:9494',
|
'GET', 'http://example.com:9494',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'})
|
||||||
data=None,
|
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'})
|
|
||||||
|
|
||||||
def test_http_300_json_request(self, mock_request):
|
def test_http_300_json_request(self, mock_request):
|
||||||
mock_request.return_value = \
|
mock_request.return_value = \
|
||||||
@@ -401,10 +360,7 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
mock_request.assert_called_once_with(
|
mock_request.assert_called_once_with(
|
||||||
'GET', 'http://example.com:9494',
|
'GET', 'http://example.com:9494',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'})
|
||||||
data=None,
|
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'})
|
|
||||||
|
|
||||||
def test_fake_json_request(self, mock_request):
|
def test_fake_json_request(self, mock_request):
|
||||||
headers = {'User-Agent': 'python-glareclient'}
|
headers = {'User-Agent': 'python-glareclient'}
|
||||||
@@ -453,10 +409,7 @@ class HttpClientTest(testtools.TestCase):
|
|||||||
mock_request.assert_called_once_with(
|
mock_request.assert_called_once_with(
|
||||||
'GET', 'http://example.com:9494',
|
'GET', 'http://example.com:9494',
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
stream=False,
|
headers={'User-Agent': 'python-glareclient'},
|
||||||
data=None,
|
|
||||||
headers={'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'python-glareclient'},
|
|
||||||
timeout=float(123))
|
timeout=float(123))
|
||||||
|
|
||||||
def test_get_system_ca_file(self, mock_request):
|
def test_get_system_ca_file(self, mock_request):
|
||||||
|
|||||||
@@ -13,63 +13,65 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import sys
|
import mock
|
||||||
|
from six import StringIO
|
||||||
import six
|
|
||||||
import testtools
|
import testtools
|
||||||
|
|
||||||
from glareclient.common import progressbar
|
from glareclient.common import progressbar
|
||||||
from glareclient.tests import utils as test_utils
|
|
||||||
|
MOD = 'glareclient.common.progressbar'
|
||||||
|
|
||||||
|
|
||||||
class TestProgressBarWrapper(testtools.TestCase):
|
class TestProgressBar(testtools.TestCase):
|
||||||
|
|
||||||
def test_iter_iterator_display_progress_bar(self):
|
@mock.patch(MOD + '.os')
|
||||||
size = 100
|
def test_totalsize_fileno(self, mock_os):
|
||||||
iterator = iter('X' * 100)
|
mock_os.fstat.return_value.st_size = 43
|
||||||
saved_stdout = sys.stdout
|
fake_file = mock.Mock()
|
||||||
try:
|
del fake_file.len
|
||||||
sys.stdout = output = test_utils.FakeTTYStdout()
|
fake_file.fileno.return_value = 42
|
||||||
# Consume iterator.
|
pb = progressbar.VerboseFileWrapper(fake_file)
|
||||||
data = list(progressbar.VerboseIteratorWrapper(iterator, size))
|
self.assertEqual(43, pb._totalsize)
|
||||||
self.assertEqual(['X'] * 100, data)
|
mock_os.fstat.assert_called_once_with(42)
|
||||||
self.assertEqual(
|
|
||||||
'[%s>] 100%%\n' % ('=' * 29),
|
|
||||||
output.getvalue()
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
sys.stdout = saved_stdout
|
|
||||||
|
|
||||||
def test_iter_file_display_progress_bar(self):
|
@mock.patch(MOD + '.sys')
|
||||||
size = 98304
|
def test__display_progress_bar(self, mock_sys):
|
||||||
file_obj = six.StringIO('X' * size)
|
fake_file = StringIO('test') # 4 bytes
|
||||||
saved_stdout = sys.stdout
|
fake_file.len = 4
|
||||||
try:
|
pb = progressbar.VerboseFileWrapper(fake_file)
|
||||||
sys.stdout = output = test_utils.FakeTTYStdout()
|
pb._display_progress_bar(2) # 2 of 4 bytes = 50%
|
||||||
file_obj = progressbar.VerboseFileWrapper(file_obj, size)
|
pb._display_progress_bar(1) # 3 of 4 bytes = 75%
|
||||||
chunksize = 1024
|
pb._display_progress_bar(1) # 4 of 4 bytes = 100%
|
||||||
chunk = file_obj.read(chunksize)
|
expected = [
|
||||||
while chunk:
|
mock.call('\r[===============> ] 50%'),
|
||||||
chunk = file_obj.read(chunksize)
|
mock.call('\r[======================> ] 75%'),
|
||||||
self.assertEqual(
|
mock.call('\r[=============================>] 100%'),
|
||||||
'[%s>] 100%%\n' % ('=' * 29),
|
]
|
||||||
output.getvalue()
|
self.assertEqual(expected, mock_sys.stdout.write.mock_calls)
|
||||||
)
|
|
||||||
finally:
|
|
||||||
sys.stdout = saved_stdout
|
|
||||||
|
|
||||||
def test_iter_file_no_tty(self):
|
@mock.patch(MOD + '.sys')
|
||||||
size = 98304
|
def test__display_progress_bar_unknown_len(self, mock_sys):
|
||||||
file_obj = six.StringIO('X' * size)
|
fake_file = StringIO('')
|
||||||
saved_stdout = sys.stdout
|
fake_file.len = 0
|
||||||
try:
|
pb = progressbar.VerboseFileWrapper(fake_file)
|
||||||
sys.stdout = output = test_utils.FakeNoTTYStdout()
|
for i in range(6):
|
||||||
file_obj = progressbar.VerboseFileWrapper(file_obj, size)
|
pb._display_progress_bar(1)
|
||||||
chunksize = 1024
|
expected = [
|
||||||
chunk = file_obj.read(chunksize)
|
mock.call('\r[-] 1 bytes'),
|
||||||
while chunk:
|
mock.call('\r[\\] 2 bytes'),
|
||||||
chunk = file_obj.read(chunksize)
|
mock.call('\r[|] 3 bytes'),
|
||||||
# If stdout is not a tty progress bar should do nothing.
|
mock.call('\r[/] 4 bytes'),
|
||||||
self.assertEqual('', output.getvalue())
|
mock.call('\r[-] 5 bytes'),
|
||||||
finally:
|
mock.call('\r[\\] 6 bytes'),
|
||||||
sys.stdout = saved_stdout
|
]
|
||||||
|
self.assertEqual(expected, mock_sys.stdout.write.mock_calls)
|
||||||
|
|
||||||
|
@mock.patch(MOD + '._ProgressBarBase.__init__')
|
||||||
|
@mock.patch(MOD + '._ProgressBarBase._display_progress_bar')
|
||||||
|
def test_read(self, mock_display_progress_bar, mock_init):
|
||||||
|
mock_init.return_value = None
|
||||||
|
pb = progressbar.VerboseFileWrapper()
|
||||||
|
pb._wrapped = mock.Mock(len=42)
|
||||||
|
pb._wrapped.read.return_value = 'ok'
|
||||||
|
pb.read(2)
|
||||||
|
mock_display_progress_bar.assert_called_once_with(2)
|
||||||
|
|||||||
@@ -14,8 +14,6 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
import mock
|
|
||||||
import six
|
|
||||||
import testtools
|
import testtools
|
||||||
|
|
||||||
from glareclient.common import utils
|
from glareclient.common import utils
|
||||||
@@ -30,36 +28,3 @@ class TestUtils(testtools.TestCase):
|
|||||||
self.assertEqual("1.4GB", utils.make_size_human_readable(1476395008))
|
self.assertEqual("1.4GB", utils.make_size_human_readable(1476395008))
|
||||||
self.assertEqual("9.3MB", utils.make_size_human_readable(9761280))
|
self.assertEqual("9.3MB", utils.make_size_human_readable(9761280))
|
||||||
self.assertEqual("0B", utils.make_size_human_readable(None))
|
self.assertEqual("0B", utils.make_size_human_readable(None))
|
||||||
|
|
||||||
def test_get_new_file_size(self):
|
|
||||||
size = 98304
|
|
||||||
file_obj = six.StringIO('X' * size)
|
|
||||||
try:
|
|
||||||
self.assertEqual(size, utils.get_file_size(file_obj))
|
|
||||||
# Check that get_file_size didn't change original file position.
|
|
||||||
self.assertEqual(0, file_obj.tell())
|
|
||||||
finally:
|
|
||||||
file_obj.close()
|
|
||||||
|
|
||||||
def test_get_consumed_file_size(self):
|
|
||||||
size, consumed = 98304, 304
|
|
||||||
file_obj = six.StringIO('X' * size)
|
|
||||||
file_obj.seek(consumed)
|
|
||||||
try:
|
|
||||||
self.assertEqual(size, utils.get_file_size(file_obj))
|
|
||||||
# Check that get_file_size didn't change original file position.
|
|
||||||
self.assertEqual(consumed, file_obj.tell())
|
|
||||||
finally:
|
|
||||||
file_obj.close()
|
|
||||||
|
|
||||||
def test_iterable_closes(self):
|
|
||||||
# Regression test for bug 1461678.
|
|
||||||
def _iterate(i):
|
|
||||||
for chunk in i:
|
|
||||||
raise(IOError)
|
|
||||||
|
|
||||||
data = six.moves.StringIO('somestring')
|
|
||||||
data.close = mock.Mock()
|
|
||||||
i = utils.IterableWithLength(data, 10)
|
|
||||||
self.assertRaises(IOError, _iterate, i)
|
|
||||||
data.close.assert_called_with()
|
|
||||||
|
|||||||
@@ -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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
import testtools
|
import testtools
|
||||||
|
|
||||||
from glareclient.exc import HTTPBadRequest
|
|
||||||
from glareclient.tests.unit.v1 import fixtures
|
|
||||||
from glareclient.tests import utils
|
|
||||||
from glareclient.v1 import artifacts
|
from glareclient.v1 import artifacts
|
||||||
|
|
||||||
|
|
||||||
class TestController(testtools.TestCase):
|
class TestController(testtools.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self.mock_resp = mock.MagicMock()
|
||||||
|
self.mock_body = mock.MagicMock()
|
||||||
|
self.mock_http_client = mock.MagicMock()
|
||||||
|
for method in ('get', 'post', 'patch', 'delete'):
|
||||||
|
method = getattr(self.mock_http_client, method)
|
||||||
|
method.return_value = (self.mock_resp, self.mock_body)
|
||||||
|
self.c = artifacts.Controller(self.mock_http_client, 'test_name')
|
||||||
|
self.c._check_type_name = mock.Mock(return_value='checked_name')
|
||||||
super(TestController, self).setUp()
|
super(TestController, self).setUp()
|
||||||
self.api = utils.FakeAPI(fixtures.data_fixtures)
|
|
||||||
self.controller = artifacts.Controller(self.api)
|
|
||||||
|
|
||||||
def test_list_artifacts(self):
|
def test_create(self):
|
||||||
artifacts = list(self.controller.list(type_name='images'))
|
body = self.c.create('name', version='0.1.2', type_name='ok')
|
||||||
self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1',
|
self.assertEqual(self.mock_body, body)
|
||||||
artifacts[0]['id'])
|
self.mock_http_client.post.assert_called_once_with(
|
||||||
self.assertEqual('art1', artifacts[0]['name'])
|
'/artifacts/checked_name',
|
||||||
self.assertEqual('db721fb0-5b85-4738-9401-f161d541de5e',
|
json={'version': '0.1.2', 'name': 'name'})
|
||||||
artifacts[1]['id'])
|
self.c._check_type_name.assert_called_once_with('ok')
|
||||||
self.assertEqual('art2', artifacts[1]['name'])
|
|
||||||
self.assertEqual('e4f027d2-bff3-4084-a2ba-f31cb5e3067f',
|
|
||||||
artifacts[2]['id'])
|
|
||||||
self.assertEqual('art3', artifacts[2]['name'])
|
|
||||||
|
|
||||||
exp_headers = {}
|
def test_update(self):
|
||||||
expect_body = None
|
remove_props = ['remove1', 'remove2']
|
||||||
expect = [('GET', '/artifacts/images?limit=20',
|
body = self.c.update('test-id', type_name='test_name',
|
||||||
exp_headers,
|
remove_props=remove_props, update1=1, update2=2)
|
||||||
expect_body)]
|
self.assertEqual(self.mock_body, body)
|
||||||
self.assertEqual(expect, self.api.calls)
|
patch_kwargs = {
|
||||||
|
'headers': {'Content-Type': 'application/json-patch+json'},
|
||||||
def test_list_with_paginate(self):
|
'json': [
|
||||||
artifacts = list(self.controller.list(type_name='images',
|
{'path': '/remove1', 'value': None, 'op': 'replace'},
|
||||||
page_size=2))
|
{'path': '/remove2', 'value': None, 'op': 'replace'},
|
||||||
self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1',
|
{'path': '/update2', 'value': 2, 'op': 'add'},
|
||||||
artifacts[0]['id'])
|
{'path': '/update1', 'value': 1, 'op': 'add'}
|
||||||
self.assertEqual('art1', artifacts[0]['name'])
|
]
|
||||||
self.assertEqual('art2', artifacts[1]['name'])
|
|
||||||
self.assertEqual('db721fb0-5b85-4738-9401-f161d541de5e',
|
|
||||||
artifacts[1]['id'])
|
|
||||||
exp_headers = {}
|
|
||||||
expect_body = None
|
|
||||||
expect = [('GET', '/artifacts/images?limit=2',
|
|
||||||
exp_headers,
|
|
||||||
expect_body),
|
|
||||||
('GET', '/artifacts/images?limit=2'
|
|
||||||
'&marker=e1090471-1d12-4935-a8d8-a9351266ece8',
|
|
||||||
exp_headers,
|
|
||||||
expect_body)]
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
|
|
||||||
def test_list_artifacts_limit(self):
|
|
||||||
artifacts = list(self.controller.list(type_name='images',
|
|
||||||
limit=2))
|
|
||||||
self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1',
|
|
||||||
artifacts[0]['id'])
|
|
||||||
self.assertEqual('art1', artifacts[0]['name'])
|
|
||||||
self.assertEqual('art2', artifacts[1]['name'])
|
|
||||||
self.assertEqual('db721fb0-5b85-4738-9401-f161d541de5e',
|
|
||||||
artifacts[1]['id'])
|
|
||||||
exp_headers = {}
|
|
||||||
expect_body = None
|
|
||||||
expect = [('GET', '/artifacts/images?limit=2',
|
|
||||||
exp_headers,
|
|
||||||
expect_body)]
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
|
|
||||||
def test_list_artifact_sort_name(self):
|
|
||||||
|
|
||||||
artifacts = list(self.controller.list(type_name='images',
|
|
||||||
sort='name:desc'))
|
|
||||||
self.assertEqual('e4f027d2-bff3-4084-a2ba-f31cb5e3067f',
|
|
||||||
artifacts[0]['id'])
|
|
||||||
self.assertEqual('art2', artifacts[0]['name'])
|
|
||||||
self.assertEqual('art1', artifacts[1]['name'])
|
|
||||||
self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1',
|
|
||||||
artifacts[1]['id'])
|
|
||||||
exp_headers = {}
|
|
||||||
expect_body = None
|
|
||||||
expect = [('GET', '/artifacts/images?limit=20'
|
|
||||||
'&sort=name%3Adesc',
|
|
||||||
exp_headers,
|
|
||||||
expect_body),
|
|
||||||
('GET', '/artifacts/images?limit=20'
|
|
||||||
'&marker=3a4560a1-e585-443e-9b39-553b46ec92d1',
|
|
||||||
exp_headers,
|
|
||||||
expect_body)]
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
|
|
||||||
def test_list_artifact_sort_badrequest(self):
|
|
||||||
with testtools.ExpectedException(HTTPBadRequest):
|
|
||||||
list(self.controller.list(type_name='images',
|
|
||||||
sort='name:KAK'))
|
|
||||||
|
|
||||||
def test_create_artifact(self):
|
|
||||||
properties = {
|
|
||||||
'name': 'art_1',
|
|
||||||
'type_name': 'images'
|
|
||||||
}
|
}
|
||||||
|
self.mock_http_client.patch.assert_called_once_with(
|
||||||
|
'/artifacts/checked_name/test-id', **patch_kwargs)
|
||||||
|
self.c._check_type_name.assert_called_once_with('test_name')
|
||||||
|
|
||||||
art = self.controller.create(**properties)
|
def test_get(self):
|
||||||
self.assertEqual('art_1', art['images'][0]['name'])
|
body = self.c.get('test-id', type_name='test_name')
|
||||||
self.assertEqual('0.0.0', art['images'][0]['version'])
|
self.assertEqual(self.mock_body, body)
|
||||||
self.assertIsNotNone(art['images'][0]['id'])
|
self.mock_http_client.get.assert_called_once_with(
|
||||||
exp_headers = {}
|
'/artifacts/checked_name/test-id')
|
||||||
expect_body = [('name', 'art_1'), ('version', '0.0.0')]
|
self.c._check_type_name.assert_called_once_with('test_name')
|
||||||
expect = [('POST', '/artifacts/images',
|
|
||||||
exp_headers,
|
|
||||||
expect_body)]
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
|
|
||||||
def test_create_artifact_bad_prop(self):
|
def test_list(self):
|
||||||
properties = {
|
self.mock_http_client.get.side_effect = [
|
||||||
'name': 'art_1',
|
(None, {'checked_name': [10, 11, 12], "next": "next1"}),
|
||||||
'type_name': 'bad_type_name',
|
(None, {'checked_name': [13, 14, 15], "next": "next2"}),
|
||||||
}
|
(None, {'checked_name': [16, 17, 18], "next": "next3"}),
|
||||||
with testtools.ExpectedException(KeyError):
|
(None, {'checked_name': [19, 20, 21]}),
|
||||||
self.controller.create(**properties)
|
]
|
||||||
|
data = list(self.c.list(type_name='test-type', limit=10, page_size=3))
|
||||||
|
expected = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
|
||||||
|
self.assertEqual(expected, data)
|
||||||
|
expected_calls = [
|
||||||
|
mock.call.get('/artifacts/checked_name?&limit=3'),
|
||||||
|
mock.call.get('next1'),
|
||||||
|
mock.call.get('next2'),
|
||||||
|
mock.call.get('next3'),
|
||||||
|
]
|
||||||
|
self.assertEqual(expected_calls, self.mock_http_client.mock_calls)
|
||||||
|
|
||||||
def test_delete_artifact(self):
|
def test_activate(self):
|
||||||
self.controller.delete(
|
self.c.update = mock.Mock()
|
||||||
artifact_id='3a4560a1-e585-443e-9b39-553b46ec92a3',
|
self.assertEqual(self.c.update.return_value,
|
||||||
type_name='images')
|
self.c.activate('test-id', type_name='test-type'))
|
||||||
|
self.c.update.assert_called_once_with('test-id', 'test-type',
|
||||||
|
status='active')
|
||||||
|
|
||||||
expect = [('DELETE', '/artifacts/images/'
|
def test_deactivate(self):
|
||||||
'3a4560a1-e585-443e-9b39-553b46ec92a3',
|
self.c.update = mock.Mock()
|
||||||
{},
|
self.assertEqual(self.c.update.return_value,
|
||||||
None)]
|
self.c.deactivate('test-id', type_name='test-type'))
|
||||||
self.assertEqual(expect, self.api.calls)
|
self.c.update.assert_called_once_with('test-id', 'test-type',
|
||||||
|
status='deactivated')
|
||||||
|
|
||||||
def test_update_prop(self):
|
def test_reactivate(self):
|
||||||
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
|
self.c.update = mock.Mock()
|
||||||
param = {'type_name': 'images',
|
self.assertEqual(self.c.update.return_value,
|
||||||
'name': 'new_name'}
|
self.c.reactivate('test-id', type_name='test-type'))
|
||||||
|
self.c.update.assert_called_once_with('test-id', 'test-type',
|
||||||
|
status='active')
|
||||||
|
|
||||||
self.controller.update(artifact_id=art_id,
|
def test_delete(self):
|
||||||
**param)
|
self.assertEqual(None, self.c.delete('test-id', type_name='test-name'))
|
||||||
|
self.mock_http_client.delete.assert_called_once_with(
|
||||||
exp_headers = {
|
'/artifacts/checked_name/test-id')
|
||||||
'Content-Type': 'application/json-patch+json'
|
|
||||||
}
|
|
||||||
|
|
||||||
expect_body = [{'path': '/name',
|
|
||||||
'value': 'new_name',
|
|
||||||
'op': 'add'}]
|
|
||||||
|
|
||||||
expect = [('PATCH', '/artifacts/images/%s' % art_id,
|
|
||||||
exp_headers,
|
|
||||||
expect_body)]
|
|
||||||
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
|
|
||||||
def test_remove_prop(self):
|
|
||||||
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
|
|
||||||
|
|
||||||
self.controller.update(artifact_id=art_id,
|
|
||||||
remove_props=['name'],
|
|
||||||
type_name='images')
|
|
||||||
exp_headers = {
|
|
||||||
'Content-Type': 'application/json-patch+json'
|
|
||||||
}
|
|
||||||
|
|
||||||
expect_body = [{'path': '/name',
|
|
||||||
'op': 'replace',
|
|
||||||
'value': None}]
|
|
||||||
|
|
||||||
expect = [('PATCH', '/artifacts/images/%s' % art_id,
|
|
||||||
exp_headers,
|
|
||||||
expect_body)]
|
|
||||||
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
self.api.calls = []
|
|
||||||
|
|
||||||
self.controller.update(artifact_id=art_id,
|
|
||||||
remove_props=['metadata/key1'],
|
|
||||||
type_name='images')
|
|
||||||
|
|
||||||
expect_body = [{'path': '/metadata/key1',
|
|
||||||
'op': 'remove'}]
|
|
||||||
|
|
||||||
expect = [('PATCH', '/artifacts/images/%s' % art_id,
|
|
||||||
exp_headers,
|
|
||||||
expect_body)]
|
|
||||||
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
self.api.calls = []
|
|
||||||
|
|
||||||
self.controller.update(artifact_id=art_id,
|
|
||||||
remove_props=['releases/1'],
|
|
||||||
type_name='images')
|
|
||||||
|
|
||||||
expect_body = [{'path': '/releases/1',
|
|
||||||
'op': 'remove'}]
|
|
||||||
|
|
||||||
expect = [('PATCH', '/artifacts/images/%s' % art_id,
|
|
||||||
exp_headers,
|
|
||||||
expect_body)]
|
|
||||||
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
|
|
||||||
def test_nontype_type_name(self):
|
|
||||||
with testtools.ExpectedException(HTTPBadRequest):
|
|
||||||
self.controller.create(name='art')
|
|
||||||
|
|
||||||
def test_active_artifact(self):
|
|
||||||
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
|
|
||||||
self.controller.activate(artifact_id=art_id,
|
|
||||||
type_name='images')
|
|
||||||
exp_headers = {
|
|
||||||
'Content-Type': 'application/json-patch+json'
|
|
||||||
}
|
|
||||||
|
|
||||||
expect_body = [{'path': '/status',
|
|
||||||
'value': 'active',
|
|
||||||
'op': 'add'}]
|
|
||||||
|
|
||||||
expect = [('PATCH', '/artifacts/images/%s' % art_id,
|
|
||||||
exp_headers,
|
|
||||||
expect_body)]
|
|
||||||
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
|
|
||||||
def test_deactivate_artifact(self):
|
|
||||||
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
|
|
||||||
self.controller.deactivate(artifact_id=art_id,
|
|
||||||
type_name='images')
|
|
||||||
exp_headers = {
|
|
||||||
'Content-Type': 'application/json-patch+json'
|
|
||||||
}
|
|
||||||
|
|
||||||
expect_body = [{'path': '/status',
|
|
||||||
'value': 'deactivated',
|
|
||||||
'op': 'add'}]
|
|
||||||
|
|
||||||
expect = [('PATCH', '/artifacts/images/%s' % art_id,
|
|
||||||
exp_headers,
|
|
||||||
expect_body)]
|
|
||||||
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
|
|
||||||
def test_reactivate_artifact(self):
|
|
||||||
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
|
|
||||||
self.controller.reactivate(artifact_id=art_id,
|
|
||||||
type_name='images')
|
|
||||||
exp_headers = {
|
|
||||||
'Content-Type': 'application/json-patch+json'
|
|
||||||
}
|
|
||||||
|
|
||||||
expect_body = [{'path': '/status',
|
|
||||||
'value': 'active',
|
|
||||||
'op': 'add'}]
|
|
||||||
|
|
||||||
expect = [('PATCH', '/artifacts/images/%s' % art_id,
|
|
||||||
exp_headers,
|
|
||||||
expect_body)]
|
|
||||||
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
|
|
||||||
def test_publish_artifact(self):
|
|
||||||
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
|
|
||||||
self.controller.publish(artifact_id=art_id,
|
|
||||||
type_name='images')
|
|
||||||
exp_headers = {
|
|
||||||
'Content-Type': 'application/json-patch+json'
|
|
||||||
}
|
|
||||||
|
|
||||||
expect_body = [{'path': '/visibility',
|
|
||||||
'value': 'public',
|
|
||||||
'op': 'add'}]
|
|
||||||
|
|
||||||
expect = [('PATCH', '/artifacts/images/%s' % art_id,
|
|
||||||
exp_headers,
|
|
||||||
expect_body)]
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
|
|
||||||
def test_upload_blob(self):
|
def test_upload_blob(self):
|
||||||
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
|
self.c.upload_blob('test-id', 'blob-prop', 'data',
|
||||||
self.controller.upload_blob(artifact_id=art_id,
|
type_name='test-type',
|
||||||
type_name='images',
|
content_type='application/test')
|
||||||
blob_property='image',
|
self.mock_http_client.put.assert_called_once_with(
|
||||||
data='data')
|
'/artifacts/checked_name/test-id/blob-prop',
|
||||||
|
data='data', headers={'Content-Type': 'application/test'})
|
||||||
|
|
||||||
exp_headers = {
|
def test_get_type_list(self):
|
||||||
'Content-Type': 'application/octet-stream'
|
schemas = {'schemas': {'a': {'version': 1}, 'b': {'version': 2}}}
|
||||||
}
|
self.mock_http_client.get.return_value = (None, schemas)
|
||||||
|
expected_types = [('a', 1), ('b', 2)]
|
||||||
|
self.assertEqual(expected_types, self.c.get_type_list())
|
||||||
|
|
||||||
expect = [('PUT', '/artifacts/images/%s/image' % art_id,
|
def test_get_type_schema(self):
|
||||||
exp_headers,
|
test_schema = {'schemas': {'checked_name': 'test-schema'}}
|
||||||
'data')]
|
self.mock_http_client.get.return_value = (None, test_schema)
|
||||||
self.assertEqual(expect, self.api.calls)
|
self.assertEqual('test-schema',
|
||||||
|
self.c.get_type_schema(type_name='test-type'))
|
||||||
def test_upload_blob_custom_content_type(self):
|
self.mock_http_client.get.assert_called_once_with(
|
||||||
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
|
'/schemas/checked_name')
|
||||||
self.controller.upload_blob(artifact_id=art_id,
|
|
||||||
type_name='images',
|
|
||||||
blob_property='image',
|
|
||||||
data='{"a":"b"}',
|
|
||||||
content_type='application/json',)
|
|
||||||
|
|
||||||
exp_headers = {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
expect = [('PUT', '/artifacts/images/%s/image' % art_id,
|
|
||||||
exp_headers,
|
|
||||||
{"a": "b"})]
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
|
|
||||||
def test_download_blob(self):
|
|
||||||
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
|
|
||||||
self.controller.download_blob(artifact_id=art_id,
|
|
||||||
type_name='images',
|
|
||||||
blob_property='image')
|
|
||||||
|
|
||||||
exp_headers = {}
|
|
||||||
|
|
||||||
expect = [('GET', '/artifacts/images/%s/image' % art_id,
|
|
||||||
exp_headers,
|
|
||||||
None)]
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
|
|
||||||
def test_download_blob_with_checksum(self):
|
|
||||||
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a2'
|
|
||||||
data = self.controller.download_blob(artifact_id=art_id,
|
|
||||||
type_name='images',
|
|
||||||
blob_property='image')
|
|
||||||
self.assertIsNotNone(data.iterable)
|
|
||||||
|
|
||||||
expect = [('GET', '/artifacts/images/%s/image' % art_id,
|
|
||||||
{},
|
|
||||||
None)]
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
|
|
||||||
def test_download_blob_without_checksum(self):
|
|
||||||
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a2'
|
|
||||||
data = self.controller.download_blob(artifact_id=art_id,
|
|
||||||
type_name='images',
|
|
||||||
blob_property='image',
|
|
||||||
do_checksum=False)
|
|
||||||
self.assertIsNotNone(data.iterable)
|
|
||||||
|
|
||||||
expect = [('GET', '/artifacts/images/%s/image' % art_id,
|
|
||||||
{},
|
|
||||||
None)]
|
|
||||||
self.assertEqual(expect, self.api.calls)
|
|
||||||
|
|
||||||
def test_get_artifact(self):
|
|
||||||
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a3'
|
|
||||||
art = self.controller.get(artifact_id=art_id,
|
|
||||||
type_name='images')
|
|
||||||
self.assertEqual(art_id, art['images'][0]['id'])
|
|
||||||
self.assertEqual('art_1', art['images'][0]['name'])
|
|
||||||
|
|
||||||
def test_get_by_name(self):
|
|
||||||
art_name = 'name1'
|
|
||||||
art = self.controller.get_by_name(name=art_name,
|
|
||||||
type_name='images')
|
|
||||||
self.assertEqual(art_name, art['name'])
|
|
||||||
self.assertEqual('3.0.0', art['version'])
|
|
||||||
|
|
||||||
def test_get_by_name_with_version(self):
|
|
||||||
art_name = 'name1'
|
|
||||||
art = self.controller.get_by_name(name=art_name,
|
|
||||||
version='1.0.0',
|
|
||||||
type_name='images')
|
|
||||||
self.assertEqual(art_name, art['name'])
|
|
||||||
self.assertEqual('1.0.0', art['version'])
|
|
||||||
|
|
||||||
def test_type_list(self):
|
|
||||||
data = self.controller.get_type_list()
|
|
||||||
expect_data = [('images', '1.0'), ('heat_environments', '1.0')]
|
|
||||||
expect_call = [('GET', '/schemas', {}, None)]
|
|
||||||
self.assertEqual(expect_call, self.api.calls)
|
|
||||||
self.assertEqual(expect_data, data)
|
|
||||||
|
|
||||||
def test_get_schema(self):
|
|
||||||
data = self.controller.get_type_schema(type_name='images')
|
|
||||||
expect_data = {'name': 'images', 'version': '1.0',
|
|
||||||
'properties': {'foo': 'bar'}}
|
|
||||||
expect_call = [('GET', '/schemas/images', {}, None)]
|
|
||||||
self.assertEqual(expect_call, self.api.calls)
|
|
||||||
self.assertEqual(expect_data, data)
|
|
||||||
|
|
||||||
def test_add_external_location(self):
|
def test_add_external_location(self):
|
||||||
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a8'
|
art_id = '3a4560a1-e585-443e-9b39-553b46ec92a8'
|
||||||
data = self.controller.add_external_location(art_id,
|
data = {
|
||||||
'image',
|
'url': 'http://fake_url',
|
||||||
'http://fake_url',
|
'md5': '7CA772EE98D5CAF99F3674085D5E4124',
|
||||||
|
'sha1': None,
|
||||||
|
'sha256': None},
|
||||||
|
resp = self.c.add_external_location(
|
||||||
|
art_id, 'image',
|
||||||
|
data=data,
|
||||||
type_name='images')
|
type_name='images')
|
||||||
expect_call = [
|
self.c.http_client.put.assert_called_once_with(
|
||||||
('PUT',
|
'/artifacts/checked_name/'
|
||||||
'/artifacts/images/3a4560a1-e585-443e-9b39-553b46ec92a8/image',
|
'3a4560a1-e585-443e-9b39-553b46ec92a8/image',
|
||||||
{'Content-Type': 'application/vnd+openstack.'
|
data=jsonutils.dumps(data),
|
||||||
'glare-custom-location+json'},
|
headers={'Content-Type':
|
||||||
'http://fake_url')]
|
'application/vnd+openstack.glare-custom-location+json'})
|
||||||
self.assertEqual(expect_call, self.api.calls)
|
self.assertIsNone(resp)
|
||||||
self.assertIsNone(data)
|
|
||||||
|
|
||||||
def test_add_tag(self):
|
def test_add_tag(self):
|
||||||
art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c'
|
art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c'
|
||||||
data = self.controller.add_tag(
|
d = {'tags': ['a', 'b', 'c']}
|
||||||
|
self.mock_body.__getitem__.side_effect = d.__getitem__
|
||||||
|
data = self.c.add_tag(
|
||||||
art_id, tag_value="123", type_name='images')
|
art_id, tag_value="123", type_name='images')
|
||||||
expect_call = [
|
self.c.http_client.get.assert_called_once_with(
|
||||||
('GET', '/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
|
'/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c')
|
||||||
{}, None),
|
self.c.http_client.patch.assert_called_once_with(
|
||||||
('PATCH',
|
'/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
|
||||||
'/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
|
headers={'Content-Type': 'application/json-patch+json'},
|
||||||
{'Content-Type': 'application/json-patch+json'},
|
json=[{'path': '/tags',
|
||||||
[{'op': 'add',
|
'value': ['a', 'b', 'c', '123'],
|
||||||
'path': '/tags',
|
'op': 'add'}])
|
||||||
'value': ['a', 'b', 'c', '123']}])]
|
|
||||||
self.assertEqual(expect_call, self.api.calls)
|
|
||||||
self.assertIsNotNone(data)
|
self.assertIsNotNone(data)
|
||||||
|
|
||||||
def test_add_existing_tag(self):
|
def test_add_existing_tag(self):
|
||||||
art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c'
|
art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c'
|
||||||
data = self.controller.add_tag(
|
d = {'tags': ['a', 'b', 'c']}
|
||||||
|
self.mock_body.__getitem__.side_effect = d.__getitem__
|
||||||
|
data = self.c.add_tag(
|
||||||
art_id, tag_value="a", type_name='images')
|
art_id, tag_value="a", type_name='images')
|
||||||
expect_call = [
|
self.c.http_client.get.assert_called_once_with(
|
||||||
('GET', '/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
|
'/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c')
|
||||||
{}, None)]
|
self.assertEqual(0, self.c.http_client.patch.call_count)
|
||||||
self.assertEqual(expect_call, self.api.calls)
|
|
||||||
self.assertIsNotNone(data)
|
self.assertIsNotNone(data)
|
||||||
|
|
||||||
def test_remove_tag(self):
|
def test_remove_tag(self):
|
||||||
art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c'
|
art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c'
|
||||||
data = self.controller.remove_tag(
|
d = {'tags': ['a', 'b', 'c']}
|
||||||
|
self.mock_body.__getitem__.side_effect = d.__getitem__
|
||||||
|
data = self.c.remove_tag(
|
||||||
art_id, tag_value="a", type_name='images')
|
art_id, tag_value="a", type_name='images')
|
||||||
expect_call = [
|
self.c.http_client.get.assert_called_once_with(
|
||||||
('GET', '/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
|
'/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c')
|
||||||
{}, None),
|
self.c.http_client.patch.assert_called_once_with(
|
||||||
('PATCH',
|
'/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
|
||||||
'/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
|
headers={'Content-Type': 'application/json-patch+json'},
|
||||||
{'Content-Type': 'application/json-patch+json'},
|
json=[{'path': '/tags',
|
||||||
[{'op': 'add',
|
'value': ['b', 'c'],
|
||||||
'path': '/tags',
|
'op': 'add'}])
|
||||||
'value': ['b', 'c']}])]
|
|
||||||
self.assertEqual(expect_call, self.api.calls)
|
|
||||||
self.assertIsNotNone(data)
|
self.assertIsNotNone(data)
|
||||||
|
|
||||||
def test_remove_nonexisting_tag(self):
|
def test_remove_nonexisting_tag(self):
|
||||||
art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c'
|
art_id = '07a679d8-d0a8-45ff-8d6e-2f32f2097b7c'
|
||||||
data = self.controller.remove_tag(
|
d = {'tags': ['a', 'b', 'c']}
|
||||||
|
self.mock_body.__getitem__.side_effect = d.__getitem__
|
||||||
|
data = self.c.remove_tag(
|
||||||
art_id, tag_value="123", type_name='images')
|
art_id, tag_value="123", type_name='images')
|
||||||
expect_call = [
|
self.c.http_client.get.assert_called_once_with(
|
||||||
('GET', '/artifacts/images/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c',
|
'/artifacts/checked_name/07a679d8-d0a8-45ff-8d6e-2f32f2097b7c')
|
||||||
{}, None)]
|
self.assertEqual(0, self.c.http_client.patch.call_count)
|
||||||
self.assertEqual(expect_call, self.api.calls)
|
|
||||||
self.assertIsNotNone(data)
|
self.assertIsNotNone(data)
|
||||||
|
|||||||
@@ -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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from glareclient.common import utils
|
from oslo_serialization import jsonutils
|
||||||
from glareclient import exc
|
|
||||||
from oslo_utils import encodeutils
|
from oslo_utils import encodeutils
|
||||||
import six
|
import six
|
||||||
from six.moves.urllib import parse
|
from six.moves.urllib import parse
|
||||||
|
|
||||||
|
from glareclient.common import utils
|
||||||
|
from glareclient import exc
|
||||||
|
|
||||||
|
|
||||||
class Controller(object):
|
class Controller(object):
|
||||||
def __init__(self, http_client, type_name=None):
|
def __init__(self, http_client, type_name=None):
|
||||||
@@ -59,7 +61,7 @@ class Controller(object):
|
|||||||
type_name = self._check_type_name(type_name)
|
type_name = self._check_type_name(type_name)
|
||||||
kwargs.update({'name': name, 'version': version})
|
kwargs.update({'name': name, 'version': version})
|
||||||
url = '/artifacts/%s' % type_name
|
url = '/artifacts/%s' % type_name
|
||||||
resp, body = self.http_client.post(url, data=kwargs)
|
resp, body = self.http_client.post(url, json=kwargs)
|
||||||
return body
|
return body
|
||||||
|
|
||||||
def update(self, artifact_id, type_name=None, remove_props=None,
|
def update(self, artifact_id, type_name=None, remove_props=None,
|
||||||
@@ -90,7 +92,7 @@ class Controller(object):
|
|||||||
for prop_name in kwargs:
|
for prop_name in kwargs:
|
||||||
changes.append({'op': 'add', 'path': '/%s' % prop_name,
|
changes.append({'op': 'add', 'path': '/%s' % prop_name,
|
||||||
'value': kwargs[prop_name]})
|
'value': kwargs[prop_name]})
|
||||||
resp, body = self.http_client.patch(url, headers=hdrs, data=changes)
|
resp, body = self.http_client.patch(url, headers=hdrs, json=changes)
|
||||||
return body
|
return body
|
||||||
|
|
||||||
def get(self, artifact_id, type_name=None):
|
def get(self, artifact_id, type_name=None):
|
||||||
@@ -226,7 +228,7 @@ class Controller(object):
|
|||||||
type_name = self._check_type_name(type_name)
|
type_name = self._check_type_name(type_name)
|
||||||
hdrs = {'Content-Type': content_type}
|
hdrs = {'Content-Type': content_type}
|
||||||
url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
|
url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
|
||||||
self.http_client.put(url, headers=hdrs, data=data, stream=True)
|
self.http_client.put(url, headers=hdrs, data=data)
|
||||||
|
|
||||||
def add_external_location(self, artifact_id, blob_property, data,
|
def add_external_location(self, artifact_id, blob_property, data,
|
||||||
type_name=None):
|
type_name=None):
|
||||||
@@ -240,6 +242,10 @@ class Controller(object):
|
|||||||
type_name = self._check_type_name(type_name)
|
type_name = self._check_type_name(type_name)
|
||||||
hdrs = {'Content-Type': content_type}
|
hdrs = {'Content-Type': content_type}
|
||||||
url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
|
url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
|
||||||
|
try:
|
||||||
|
data = jsonutils.dumps(data)
|
||||||
|
except TypeError:
|
||||||
|
raise exc.HTTPBadRequest("json is malformed.")
|
||||||
self.http_client.put(url, headers=hdrs, data=data)
|
self.http_client.put(url, headers=hdrs, data=data)
|
||||||
|
|
||||||
def download_blob(self, artifact_id, blob_property, type_name=None,
|
def download_blob(self, artifact_id, blob_property, type_name=None,
|
||||||
@@ -252,12 +258,10 @@ class Controller(object):
|
|||||||
"""
|
"""
|
||||||
type_name = self._check_type_name(type_name)
|
type_name = self._check_type_name(type_name)
|
||||||
url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
|
url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
|
||||||
resp, body = self.http_client.get(url)
|
resp, body = self.http_client.get(url, redirect=False,
|
||||||
checksum = resp.headers.get('content-md5', None)
|
stream=True,
|
||||||
content_length = int(resp.headers.get('content-length', 0))
|
headers={"Accept": "*/*"})
|
||||||
if checksum is not None and do_checksum:
|
return utils.ResponseBlobWrapper(resp, do_checksum)
|
||||||
body = utils.integrity_iter(body, checksum)
|
|
||||||
return utils.IterableWithLength(body, content_length)
|
|
||||||
|
|
||||||
def get_type_list(self):
|
def get_type_list(self):
|
||||||
"""Get list of type names."""
|
"""Get list of type names."""
|
||||||
|
|||||||
Reference in New Issue
Block a user