Migrate rest_client to tempest-lib from tempest
This patch migrates rest_client module to tempest-lib from tempest. The latest Change-Ids of each file are the following when this migration: * common/http.py : I43703e2289212389c7841f44691ae7849ed1f505 * common/rest_client.py : Ie9105b5d01e7883213c1d3398cc5fe56782920d9 * common/utils/misc.py : I9a591eaa1cf4dabba58f06a64814611a05a51365 * exceptions.py : Ic8fc216377942619f11a2462b79d0597071ac294 * tests/base.py : I8f14cd2ca6afc38d3fe8ee758272071111022896 * tests/fake_auth_provider.py: Id12341de52204e2c428e10b4b758b700b0fbab09 * tests/fake_http.py : I8f14cd2ca6afc38d3fe8ee758272071111022896 * tests/test_rest_client.py : Ie9105b5d01e7883213c1d3398cc5fe56782920d9 NOTE: Some docstrings are changed to avoid H404 and H405. Change-Id: I879a02681c99376ae57458a0f7a04c8032dfebb2
This commit is contained in:
parent
d588748a36
commit
84dee6b781
@ -6,3 +6,6 @@ Babel>=1.3
|
||||
fixtures>=0.3.14
|
||||
oslo.config>=1.4.0 # Apache-2.0
|
||||
iso8601>=0.1.9
|
||||
jsonschema>=2.0.0,<3.0.0
|
||||
httplib2>=0.7.5
|
||||
six>=1.7.0
|
||||
|
0
tempest_lib/common/__init__.py
Normal file
0
tempest_lib/common/__init__.py
Normal file
25
tempest_lib/common/http.py
Normal file
25
tempest_lib/common/http.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright 2013 Citrix Systems, Inc.
|
||||
# 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 httplib2
|
||||
|
||||
|
||||
class ClosingHttp(httplib2.Http):
|
||||
def request(self, *args, **kwargs):
|
||||
original_headers = kwargs.get('headers', {})
|
||||
new_headers = dict(original_headers, connection='close')
|
||||
new_kwargs = dict(kwargs, headers=new_headers)
|
||||
return super(ClosingHttp, self).request(*args, **new_kwargs)
|
561
tempest_lib/common/rest_client.py
Normal file
561
tempest_lib/common/rest_client.py
Normal file
@ -0,0 +1,561 @@
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
# Copyright 2013 IBM Corp.
|
||||
# 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 collections
|
||||
import json
|
||||
import logging as real_logging
|
||||
import re
|
||||
import time
|
||||
|
||||
import jsonschema
|
||||
import six
|
||||
|
||||
from tempest_lib.common import http
|
||||
from tempest_lib.common.utils import misc as misc_utils
|
||||
from tempest_lib import exceptions
|
||||
from tempest_lib.openstack.common import log as logging
|
||||
|
||||
# redrive rate limited calls at most twice
|
||||
MAX_RECURSION_DEPTH = 2
|
||||
|
||||
# All the successful HTTP status codes from RFC 7231 & 4918
|
||||
HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207)
|
||||
|
||||
|
||||
class RestClient(object):
|
||||
|
||||
TYPE = "json"
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
def __init__(self, auth_provider, service, region,
|
||||
endpoint_type='publicURL',
|
||||
build_interval=1, build_timeout=60,
|
||||
disable_ssl_certificate_validation=False, ca_certs=None,
|
||||
trace_requests=''):
|
||||
self.auth_provider = auth_provider
|
||||
self.service = service
|
||||
self.region = region
|
||||
self.endpoint_type = endpoint_type
|
||||
self.build_interval = build_interval
|
||||
self.build_timeout = build_timeout
|
||||
self.trace_requests = trace_requests
|
||||
|
||||
# The version of the API this client implements
|
||||
self.api_version = None
|
||||
self._skip_path = False
|
||||
self.general_header_lc = set(('cache-control', 'connection',
|
||||
'date', 'pragma', 'trailer',
|
||||
'transfer-encoding', 'via',
|
||||
'warning'))
|
||||
self.response_header_lc = set(('accept-ranges', 'age', 'etag',
|
||||
'location', 'proxy-authenticate',
|
||||
'retry-after', 'server',
|
||||
'vary', 'www-authenticate'))
|
||||
dscv = disable_ssl_certificate_validation
|
||||
self.http_obj = http.ClosingHttp(
|
||||
disable_ssl_certificate_validation=dscv, ca_certs=ca_certs)
|
||||
|
||||
def _get_type(self):
|
||||
return self.TYPE
|
||||
|
||||
def get_headers(self, accept_type=None, send_type=None):
|
||||
if accept_type is None:
|
||||
accept_type = self._get_type()
|
||||
if send_type is None:
|
||||
send_type = self._get_type()
|
||||
return {'Content-Type': 'application/%s' % send_type,
|
||||
'Accept': 'application/%s' % accept_type}
|
||||
|
||||
def __str__(self):
|
||||
STRING_LIMIT = 80
|
||||
str_format = ("service:%s, base_url:%s, "
|
||||
"filters: %s, build_interval:%s, build_timeout:%s"
|
||||
"\ntoken:%s..., \nheaders:%s...")
|
||||
return str_format % (self.service, self.base_url,
|
||||
self.filters, self.build_interval,
|
||||
self.build_timeout,
|
||||
str(self.token)[0:STRING_LIMIT],
|
||||
str(self.get_headers())[0:STRING_LIMIT])
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self.auth_provider.credentials.username
|
||||
|
||||
@property
|
||||
def user_id(self):
|
||||
return self.auth_provider.credentials.user_id
|
||||
|
||||
@property
|
||||
def tenant_name(self):
|
||||
return self.auth_provider.credentials.tenant_name
|
||||
|
||||
@property
|
||||
def tenant_id(self):
|
||||
return self.auth_provider.credentials.tenant_id
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
return self.auth_provider.credentials.password
|
||||
|
||||
@property
|
||||
def base_url(self):
|
||||
return self.auth_provider.base_url(filters=self.filters)
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
return self.auth_provider.get_token()
|
||||
|
||||
@property
|
||||
def filters(self):
|
||||
_filters = dict(
|
||||
service=self.service,
|
||||
endpoint_type=self.endpoint_type,
|
||||
region=self.region
|
||||
)
|
||||
if self.api_version is not None:
|
||||
_filters['api_version'] = self.api_version
|
||||
if self._skip_path:
|
||||
_filters['skip_path'] = self._skip_path
|
||||
return _filters
|
||||
|
||||
def skip_path(self):
|
||||
"""When set, ignore the path part of the base URL from the catalog"""
|
||||
self._skip_path = True
|
||||
|
||||
def reset_path(self):
|
||||
"""When reset, use the base URL from the catalog as-is"""
|
||||
self._skip_path = False
|
||||
|
||||
@classmethod
|
||||
def expected_success(cls, expected_code, read_code):
|
||||
assert_msg = ("This function only allowed to use for HTTP status"
|
||||
"codes which explicitly defined in the RFC 7231 & 4918."
|
||||
"{0} is not a defined Success Code!"
|
||||
).format(expected_code)
|
||||
if isinstance(expected_code, list):
|
||||
for code in expected_code:
|
||||
assert code in HTTP_SUCCESS, assert_msg
|
||||
else:
|
||||
assert expected_code in HTTP_SUCCESS, assert_msg
|
||||
|
||||
# NOTE(afazekas): the http status code above 400 is processed by
|
||||
# the _error_checker method
|
||||
if read_code < 400:
|
||||
pattern = """Unexpected http success status code {0},
|
||||
The expected status code is {1}"""
|
||||
if ((not isinstance(expected_code, list) and
|
||||
(read_code != expected_code)) or
|
||||
(isinstance(expected_code, list) and
|
||||
(read_code not in expected_code))):
|
||||
details = pattern.format(read_code, expected_code)
|
||||
raise exceptions.InvalidHttpSuccessCode(details)
|
||||
|
||||
def post(self, url, body, headers=None, extra_headers=False):
|
||||
return self.request('POST', url, extra_headers, headers, body)
|
||||
|
||||
def get(self, url, headers=None, extra_headers=False):
|
||||
return self.request('GET', url, extra_headers, headers)
|
||||
|
||||
def delete(self, url, headers=None, body=None, extra_headers=False):
|
||||
return self.request('DELETE', url, extra_headers, headers, body)
|
||||
|
||||
def patch(self, url, body, headers=None, extra_headers=False):
|
||||
return self.request('PATCH', url, extra_headers, headers, body)
|
||||
|
||||
def put(self, url, body, headers=None, extra_headers=False):
|
||||
return self.request('PUT', url, extra_headers, headers, body)
|
||||
|
||||
def head(self, url, headers=None, extra_headers=False):
|
||||
return self.request('HEAD', url, extra_headers, headers)
|
||||
|
||||
def copy(self, url, headers=None, extra_headers=False):
|
||||
return self.request('COPY', url, extra_headers, headers)
|
||||
|
||||
def get_versions(self):
|
||||
resp, body = self.get('')
|
||||
body = self._parse_resp(body)
|
||||
versions = map(lambda x: x['id'], body)
|
||||
return resp, versions
|
||||
|
||||
def _get_request_id(self, resp):
|
||||
for i in ('x-openstack-request-id', 'x-compute-request-id'):
|
||||
if i in resp:
|
||||
return resp[i]
|
||||
return ""
|
||||
|
||||
def _safe_body(self, body, maxlen=4096):
|
||||
# convert a structure into a string safely
|
||||
try:
|
||||
text = six.text_type(body)
|
||||
except UnicodeDecodeError:
|
||||
# if this isn't actually text, return marker that
|
||||
return "<BinaryData: removed>"
|
||||
if len(text) > maxlen:
|
||||
return text[:maxlen]
|
||||
else:
|
||||
return text
|
||||
|
||||
def _log_request_start(self, method, req_url, req_headers=None,
|
||||
req_body=None):
|
||||
if req_headers is None:
|
||||
req_headers = {}
|
||||
caller_name = misc_utils.find_test_caller()
|
||||
if self.trace_requests and re.search(self.trace_requests, caller_name):
|
||||
self.LOG.debug('Starting Request (%s): %s %s' %
|
||||
(caller_name, method, req_url))
|
||||
|
||||
def _log_request_full(self, method, req_url, resp,
|
||||
secs="", req_headers=None,
|
||||
req_body=None, resp_body=None,
|
||||
caller_name=None, extra=None):
|
||||
if 'X-Auth-Token' in req_headers:
|
||||
req_headers['X-Auth-Token'] = '<omitted>'
|
||||
log_fmt = """Request (%s): %s %s %s%s
|
||||
Request - Headers: %s
|
||||
Body: %s
|
||||
Response - Headers: %s
|
||||
Body: %s"""
|
||||
|
||||
self.LOG.debug(
|
||||
log_fmt % (
|
||||
caller_name,
|
||||
resp['status'],
|
||||
method,
|
||||
req_url,
|
||||
secs,
|
||||
str(req_headers),
|
||||
self._safe_body(req_body),
|
||||
str(resp),
|
||||
self._safe_body(resp_body)),
|
||||
extra=extra)
|
||||
|
||||
def _log_request(self, method, req_url, resp,
|
||||
secs="", req_headers=None,
|
||||
req_body=None, resp_body=None):
|
||||
if req_headers is None:
|
||||
req_headers = {}
|
||||
# if we have the request id, put it in the right part of the log
|
||||
extra = dict(request_id=self._get_request_id(resp))
|
||||
# NOTE(sdague): while we still have 6 callers to this function
|
||||
# we're going to just provide work around on who is actually
|
||||
# providing timings by gracefully adding no content if they don't.
|
||||
# Once we're down to 1 caller, clean this up.
|
||||
caller_name = misc_utils.find_test_caller()
|
||||
if secs:
|
||||
secs = " %.3fs" % secs
|
||||
if not self.LOG.isEnabledFor(real_logging.DEBUG):
|
||||
self.LOG.info(
|
||||
'Request (%s): %s %s %s%s' % (
|
||||
caller_name,
|
||||
resp['status'],
|
||||
method,
|
||||
req_url,
|
||||
secs),
|
||||
extra=extra)
|
||||
|
||||
# Also look everything at DEBUG if you want to filter this
|
||||
# out, don't run at debug.
|
||||
self._log_request_full(method, req_url, resp, secs, req_headers,
|
||||
req_body, resp_body, caller_name, extra)
|
||||
|
||||
def _parse_resp(self, body):
|
||||
body = json.loads(body)
|
||||
|
||||
# We assume, that if the first value of the deserialized body's
|
||||
# item set is a dict or a list, that we just return the first value
|
||||
# of deserialized body.
|
||||
# Essentially "cutting out" the first placeholder element in a body
|
||||
# that looks like this:
|
||||
#
|
||||
# {
|
||||
# "users": [
|
||||
# ...
|
||||
# ]
|
||||
# }
|
||||
try:
|
||||
# Ensure there are not more than one top-level keys
|
||||
if len(body.keys()) > 1:
|
||||
return body
|
||||
# Just return the "wrapped" element
|
||||
first_key, first_item = six.next(six.iteritems(body))
|
||||
if isinstance(first_item, (dict, list)):
|
||||
return first_item
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
return body
|
||||
|
||||
def response_checker(self, method, resp, resp_body):
|
||||
if (resp.status in set((204, 205, 304)) or resp.status < 200 or
|
||||
method.upper() == 'HEAD') and resp_body:
|
||||
raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
|
||||
# NOTE(afazekas):
|
||||
# If the HTTP Status Code is 205
|
||||
# 'The response MUST NOT include an entity.'
|
||||
# A HTTP entity has an entity-body and an 'entity-header'.
|
||||
# In the HTTP response specification (Section 6) the 'entity-header'
|
||||
# 'generic-header' and 'response-header' are in OR relation.
|
||||
# All headers not in the above two group are considered as entity
|
||||
# header in every interpretation.
|
||||
|
||||
if (resp.status == 205 and
|
||||
0 != len(set(resp.keys()) - set(('status',)) -
|
||||
self.response_header_lc - self.general_header_lc)):
|
||||
raise exceptions.ResponseWithEntity()
|
||||
# NOTE(afazekas)
|
||||
# Now the swift sometimes (delete not empty container)
|
||||
# returns with non json error response, we can create new rest class
|
||||
# for swift.
|
||||
# Usually RFC2616 says error responses SHOULD contain an explanation.
|
||||
# The warning is normal for SHOULD/SHOULD NOT case
|
||||
|
||||
# Likely it will cause an error
|
||||
if method != 'HEAD' and not resp_body and resp.status >= 400:
|
||||
self.LOG.warning("status >= 400 response with empty body")
|
||||
|
||||
def _request(self, method, url, headers=None, body=None):
|
||||
"""A simple HTTP request interface."""
|
||||
# Authenticate the request with the auth provider
|
||||
req_url, req_headers, req_body = self.auth_provider.auth_request(
|
||||
method, url, headers, body, self.filters)
|
||||
|
||||
# Do the actual request, and time it
|
||||
start = time.time()
|
||||
self._log_request_start(method, req_url)
|
||||
resp, resp_body = self.raw_request(
|
||||
req_url, method, headers=req_headers, body=req_body)
|
||||
end = time.time()
|
||||
self._log_request(method, req_url, resp, secs=(end - start),
|
||||
req_headers=req_headers, req_body=req_body,
|
||||
resp_body=resp_body)
|
||||
|
||||
# Verify HTTP response codes
|
||||
self.response_checker(method, resp, resp_body)
|
||||
|
||||
return resp, resp_body
|
||||
|
||||
def raw_request(self, url, method, headers=None, body=None):
|
||||
if headers is None:
|
||||
headers = self.get_headers()
|
||||
return self.http_obj.request(url, method,
|
||||
headers=headers, body=body)
|
||||
|
||||
def request(self, method, url, extra_headers=False, headers=None,
|
||||
body=None):
|
||||
# if extra_headers is True
|
||||
# default headers would be added to headers
|
||||
retry = 0
|
||||
|
||||
if headers is None:
|
||||
# NOTE(vponomaryov): if some client do not need headers,
|
||||
# it should explicitly pass empty dict
|
||||
headers = self.get_headers()
|
||||
elif extra_headers:
|
||||
try:
|
||||
headers = headers.copy()
|
||||
headers.update(self.get_headers())
|
||||
except (ValueError, TypeError):
|
||||
headers = self.get_headers()
|
||||
|
||||
resp, resp_body = self._request(method, url,
|
||||
headers=headers, body=body)
|
||||
|
||||
while (resp.status == 413 and
|
||||
'retry-after' in resp and
|
||||
not self.is_absolute_limit(
|
||||
resp, self._parse_resp(resp_body)) and
|
||||
retry < MAX_RECURSION_DEPTH):
|
||||
retry += 1
|
||||
delay = int(resp['retry-after'])
|
||||
time.sleep(delay)
|
||||
resp, resp_body = self._request(method, url,
|
||||
headers=headers, body=body)
|
||||
self._error_checker(method, url, headers, body,
|
||||
resp, resp_body)
|
||||
return resp, resp_body
|
||||
|
||||
def _error_checker(self, method, url,
|
||||
headers, body, resp, resp_body):
|
||||
|
||||
# NOTE(mtreinish): Check for httplib response from glance_http. The
|
||||
# object can't be used here because importing httplib breaks httplib2.
|
||||
# If another object from a class not imported were passed here as
|
||||
# resp this could possibly fail
|
||||
if str(type(resp)) == "<type 'instance'>":
|
||||
ctype = resp.getheader('content-type')
|
||||
else:
|
||||
try:
|
||||
ctype = resp['content-type']
|
||||
# NOTE(mtreinish): Keystone delete user responses doesn't have a
|
||||
# content-type header. (They don't have a body) So just pretend it
|
||||
# is set.
|
||||
except KeyError:
|
||||
ctype = 'application/json'
|
||||
|
||||
# It is not an error response
|
||||
if resp.status < 400:
|
||||
return
|
||||
|
||||
JSON_ENC = ['application/json', 'application/json; charset=utf-8']
|
||||
# NOTE(mtreinish): This is for compatibility with Glance and swift
|
||||
# APIs. These are the return content types that Glance api v1
|
||||
# (and occasionally swift) are using.
|
||||
TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
|
||||
'text/plain; charset=utf-8']
|
||||
|
||||
if ctype.lower() in JSON_ENC:
|
||||
parse_resp = True
|
||||
elif ctype.lower() in TXT_ENC:
|
||||
parse_resp = False
|
||||
else:
|
||||
raise exceptions.InvalidContentType(str(resp.status))
|
||||
|
||||
if resp.status == 401 or resp.status == 403:
|
||||
raise exceptions.Unauthorized(resp_body)
|
||||
|
||||
if resp.status == 404:
|
||||
raise exceptions.NotFound(resp_body)
|
||||
|
||||
if resp.status == 400:
|
||||
if parse_resp:
|
||||
resp_body = self._parse_resp(resp_body)
|
||||
raise exceptions.BadRequest(resp_body)
|
||||
|
||||
if resp.status == 409:
|
||||
if parse_resp:
|
||||
resp_body = self._parse_resp(resp_body)
|
||||
raise exceptions.Conflict(resp_body)
|
||||
|
||||
if resp.status == 413:
|
||||
if parse_resp:
|
||||
resp_body = self._parse_resp(resp_body)
|
||||
if self.is_absolute_limit(resp, resp_body):
|
||||
raise exceptions.OverLimit(resp_body)
|
||||
else:
|
||||
raise exceptions.RateLimitExceeded(resp_body)
|
||||
|
||||
if resp.status == 415:
|
||||
if parse_resp:
|
||||
resp_body = self._parse_resp(resp_body)
|
||||
raise exceptions.InvalidContentType(resp_body)
|
||||
|
||||
if resp.status == 422:
|
||||
if parse_resp:
|
||||
resp_body = self._parse_resp(resp_body)
|
||||
raise exceptions.UnprocessableEntity(resp_body)
|
||||
|
||||
if resp.status in (500, 501):
|
||||
message = resp_body
|
||||
if parse_resp:
|
||||
try:
|
||||
resp_body = self._parse_resp(resp_body)
|
||||
except ValueError:
|
||||
# If response body is a non-json string message.
|
||||
# Use resp_body as is and raise InvalidResponseBody
|
||||
# exception.
|
||||
raise exceptions.InvalidHTTPResponseBody(message)
|
||||
else:
|
||||
if isinstance(resp_body, dict):
|
||||
# I'm seeing both computeFault
|
||||
# and cloudServersFault come back.
|
||||
# Will file a bug to fix, but leave as is for now.
|
||||
if 'cloudServersFault' in resp_body:
|
||||
message = resp_body['cloudServersFault']['message']
|
||||
elif 'computeFault' in resp_body:
|
||||
message = resp_body['computeFault']['message']
|
||||
elif 'error' in resp_body:
|
||||
message = resp_body['error']['message']
|
||||
elif 'message' in resp_body:
|
||||
message = resp_body['message']
|
||||
else:
|
||||
message = resp_body
|
||||
|
||||
if resp.status == 501:
|
||||
raise exceptions.NotImplemented(message)
|
||||
else:
|
||||
raise exceptions.ServerFault(message)
|
||||
|
||||
if resp.status >= 400:
|
||||
raise exceptions.UnexpectedResponseCode(str(resp.status))
|
||||
|
||||
def is_absolute_limit(self, resp, resp_body):
|
||||
if (not isinstance(resp_body, collections.Mapping) or
|
||||
'retry-after' not in resp):
|
||||
return True
|
||||
over_limit = resp_body.get('overLimit', None)
|
||||
if not over_limit:
|
||||
return True
|
||||
return 'exceed' in over_limit.get('message', 'blabla')
|
||||
|
||||
def wait_for_resource_deletion(self, id):
|
||||
"""Waits for a resource to be deleted."""
|
||||
start_time = int(time.time())
|
||||
while True:
|
||||
if self.is_resource_deleted(id):
|
||||
return
|
||||
if int(time.time()) - start_time >= self.build_timeout:
|
||||
message = ('Failed to delete %(resource_type)s %(id)s within '
|
||||
'the required time (%(timeout)s s).' %
|
||||
{'resource_type': self.resource_type, 'id': id,
|
||||
'timeout': self.build_timeout})
|
||||
caller = misc_utils.find_test_caller()
|
||||
if caller:
|
||||
message = '(%s) %s' % (caller, message)
|
||||
raise exceptions.TimeoutException(message)
|
||||
time.sleep(self.build_interval)
|
||||
|
||||
def is_resource_deleted(self, id):
|
||||
"""Subclasses override with specific deletion detection."""
|
||||
message = ('"%s" does not implement is_resource_deleted'
|
||||
% self.__class__.__name__)
|
||||
raise NotImplementedError(message)
|
||||
|
||||
@property
|
||||
def resource_type(self):
|
||||
"""Returns the primary type of resource this client works with."""
|
||||
return 'resource'
|
||||
|
||||
@classmethod
|
||||
def validate_response(cls, schema, resp, body):
|
||||
# Only check the response if the status code is a success code
|
||||
# TODO(cyeoh): Eventually we should be able to verify that a failure
|
||||
# code if it exists is something that we expect. This is explicitly
|
||||
# declared in the V3 API and so we should be able to export this in
|
||||
# the response schema. For now we'll ignore it.
|
||||
if resp.status in HTTP_SUCCESS:
|
||||
cls.expected_success(schema['status_code'], resp.status)
|
||||
|
||||
# Check the body of a response
|
||||
body_schema = schema.get('response_body')
|
||||
if body_schema:
|
||||
try:
|
||||
jsonschema.validate(body, body_schema)
|
||||
except jsonschema.ValidationError as ex:
|
||||
msg = ("HTTP response body is invalid (%s)") % ex
|
||||
raise exceptions.InvalidHTTPResponseBody(msg)
|
||||
else:
|
||||
if body:
|
||||
msg = ("HTTP response body should not exist (%s)") % body
|
||||
raise exceptions.InvalidHTTPResponseBody(msg)
|
||||
|
||||
# Check the header of a response
|
||||
header_schema = schema.get('response_header')
|
||||
if header_schema:
|
||||
try:
|
||||
jsonschema.validate(resp, header_schema)
|
||||
except jsonschema.ValidationError as ex:
|
||||
msg = ("HTTP response header is invalid (%s)") % ex
|
||||
raise exceptions.InvalidHTTPResponseHeader(msg)
|
0
tempest_lib/common/utils/__init__.py
Normal file
0
tempest_lib/common/utils/__init__.py
Normal file
87
tempest_lib/common/utils/misc.py
Normal file
87
tempest_lib/common/utils/misc.py
Normal file
@ -0,0 +1,87 @@
|
||||
# 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 inspect
|
||||
import re
|
||||
|
||||
from tempest_lib.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def singleton(cls):
|
||||
"""Simple wrapper for classes that should only have a single instance."""
|
||||
instances = {}
|
||||
|
||||
def getinstance():
|
||||
if cls not in instances:
|
||||
instances[cls] = cls()
|
||||
return instances[cls]
|
||||
return getinstance
|
||||
|
||||
|
||||
def find_test_caller():
|
||||
"""Find the caller class and test name.
|
||||
|
||||
Because we know that the interesting things that call us are
|
||||
test_* methods, and various kinds of setUp / tearDown, we
|
||||
can look through the call stack to find appropriate methods,
|
||||
and the class we were in when those were called.
|
||||
"""
|
||||
caller_name = None
|
||||
names = []
|
||||
frame = inspect.currentframe()
|
||||
is_cleanup = False
|
||||
# Start climbing the ladder until we hit a good method
|
||||
while True:
|
||||
try:
|
||||
frame = frame.f_back
|
||||
name = frame.f_code.co_name
|
||||
names.append(name)
|
||||
if re.search("^(test_|setUp|tearDown)", name):
|
||||
cname = ""
|
||||
if 'self' in frame.f_locals:
|
||||
cname = frame.f_locals['self'].__class__.__name__
|
||||
if 'cls' in frame.f_locals:
|
||||
cname = frame.f_locals['cls'].__name__
|
||||
caller_name = cname + ":" + name
|
||||
break
|
||||
elif re.search("^_run_cleanup", name):
|
||||
is_cleanup = True
|
||||
elif name == 'main':
|
||||
caller_name = 'main'
|
||||
break
|
||||
else:
|
||||
cname = ""
|
||||
if 'self' in frame.f_locals:
|
||||
cname = frame.f_locals['self'].__class__.__name__
|
||||
if 'cls' in frame.f_locals:
|
||||
cname = frame.f_locals['cls'].__name__
|
||||
|
||||
# the fact that we are running cleanups is indicated pretty
|
||||
# deep in the stack, so if we see that we want to just
|
||||
# start looking for a real class name, and declare victory
|
||||
# once we do.
|
||||
if is_cleanup and cname:
|
||||
if not re.search("^RunTest", cname):
|
||||
caller_name = cname + ":_run_cleanups"
|
||||
break
|
||||
except Exception:
|
||||
break
|
||||
# prevents frame leaks
|
||||
del frame
|
||||
if caller_name is None:
|
||||
LOG.debug("Sane call name not found in %s" % names)
|
||||
return caller_name
|
@ -17,13 +17,12 @@ import testtools
|
||||
|
||||
|
||||
class TempestException(Exception):
|
||||
"""Base Tempest Exception.
|
||||
"""Base Tempest Exception
|
||||
|
||||
To correctly use this class, inherit from it and define
|
||||
a 'message' property. That message will get printf'd
|
||||
with the keyword arguments provided to the constructor.
|
||||
"""
|
||||
|
||||
message = "An unknown exception occurred"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -75,7 +74,7 @@ class Unauthorized(RestClientException):
|
||||
message = 'Unauthorized'
|
||||
|
||||
|
||||
class InvalidServiceTag(RestClientException):
|
||||
class InvalidServiceTag(TempestException):
|
||||
message = "Invalid service tag"
|
||||
|
||||
|
||||
@ -140,18 +139,22 @@ class EndpointNotFound(TempestException):
|
||||
message = "Endpoint not found"
|
||||
|
||||
|
||||
class RateLimitExceeded(TempestException):
|
||||
class RateLimitExceeded(RestClientException):
|
||||
message = "Rate limit exceeded"
|
||||
|
||||
|
||||
class OverLimit(TempestException):
|
||||
class OverLimit(RestClientException):
|
||||
message = "Quota exceeded"
|
||||
|
||||
|
||||
class ServerFault(TempestException):
|
||||
class ServerFault(RestClientException):
|
||||
message = "Got server fault"
|
||||
|
||||
|
||||
class NotImplemented(RestClientException):
|
||||
message = "Got NotImplemented error"
|
||||
|
||||
|
||||
class ImageFault(TempestException):
|
||||
message = "Got image fault"
|
||||
|
||||
|
@ -1,53 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2010-2011 OpenStack Foundation
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
# 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 os
|
||||
|
||||
import fixtures
|
||||
import testtools
|
||||
|
||||
_TRUE_VALUES = ('True', 'true', '1', 'yes')
|
||||
import mock
|
||||
from oslotest import base
|
||||
from oslotest import moxstubout
|
||||
|
||||
|
||||
class TestCase(testtools.TestCase):
|
||||
|
||||
"""Test case base class for all unit tests."""
|
||||
class TestCase(base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Run before each test method to initialize test environment."""
|
||||
|
||||
super(TestCase, self).setUp()
|
||||
test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
|
||||
try:
|
||||
test_timeout = int(test_timeout)
|
||||
except ValueError:
|
||||
# If timeout value is invalid do not set a timeout.
|
||||
test_timeout = 0
|
||||
if test_timeout > 0:
|
||||
self.useFixture(fixtures.Timeout(test_timeout, gentle=True))
|
||||
mox_fixture = self.useFixture(moxstubout.MoxStubout())
|
||||
self.mox = mox_fixture.mox
|
||||
self.stubs = mox_fixture.stubs
|
||||
|
||||
self.useFixture(fixtures.NestedTempfile())
|
||||
self.useFixture(fixtures.TempHomeDir())
|
||||
def patch(self, target, **kwargs):
|
||||
"""Returns a started `mock.patch` object for the supplied target.
|
||||
|
||||
if os.environ.get('OS_STDOUT_CAPTURE') in _TRUE_VALUES:
|
||||
stdout = self.useFixture(fixtures.StringStream('stdout')).stream
|
||||
self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
|
||||
if os.environ.get('OS_STDERR_CAPTURE') in _TRUE_VALUES:
|
||||
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
|
||||
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
|
||||
The caller may then call the returned patcher to create a mock object.
|
||||
|
||||
self.log_fixture = self.useFixture(fixtures.FakeLogger())
|
||||
The caller does not need to call stop() on the returned
|
||||
patcher object, as this method automatically adds a cleanup
|
||||
to the test class to stop the patcher.
|
||||
|
||||
:param target: String module.class or module.object expression to patch
|
||||
:param **kwargs: Passed as-is to `mock.patch`. See mock documentation
|
||||
for details.
|
||||
"""
|
||||
p = mock.patch(target, **kwargs)
|
||||
m = p.start()
|
||||
self.addCleanup(p.stop)
|
||||
return m
|
||||
|
20
tempest_lib/tests/fake_auth_provider.py
Normal file
20
tempest_lib/tests/fake_auth_provider.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
# 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.
|
||||
|
||||
|
||||
class FakeAuthProvider(object):
|
||||
|
||||
def auth_request(self, method, url, headers=None, body=None, filters=None):
|
||||
return url, headers, body
|
74
tempest_lib/tests/fake_http.py
Normal file
74
tempest_lib/tests/fake_http.py
Normal file
@ -0,0 +1,74 @@
|
||||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# 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 httplib2
|
||||
|
||||
|
||||
class fake_httplib2(object):
|
||||
|
||||
def __init__(self, return_type=None, *args, **kwargs):
|
||||
self.return_type = return_type
|
||||
|
||||
def request(self, uri, method="GET", body=None, headers=None,
|
||||
redirections=5, connection_type=None):
|
||||
if not self.return_type:
|
||||
fake_headers = httplib2.Response(headers)
|
||||
return_obj = {
|
||||
'uri': uri,
|
||||
'method': method,
|
||||
'body': body,
|
||||
'headers': headers
|
||||
}
|
||||
return (fake_headers, return_obj)
|
||||
elif isinstance(self.return_type, int):
|
||||
body = "fake_body"
|
||||
header_info = {
|
||||
'content-type': 'text/plain',
|
||||
'status': str(self.return_type),
|
||||
'content-length': len(body)
|
||||
}
|
||||
resp_header = httplib2.Response(header_info)
|
||||
return (resp_header, body)
|
||||
else:
|
||||
msg = "unsupported return type %s" % self.return_type
|
||||
raise TypeError(msg)
|
||||
|
||||
|
||||
class fake_httplib(object):
|
||||
def __init__(self, headers, body=None,
|
||||
version=1.0, status=200, reason="Ok"):
|
||||
"""fake_httplib for tests
|
||||
|
||||
: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)
|
470
tempest_lib/tests/test_rest_client.py
Normal file
470
tempest_lib/tests/test_rest_client.py
Normal file
@ -0,0 +1,470 @@
|
||||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# 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 json
|
||||
|
||||
import httplib2
|
||||
from oslotest import mockpatch
|
||||
import six
|
||||
|
||||
from tempest_lib.common import rest_client
|
||||
from tempest_lib import exceptions
|
||||
from tempest_lib.tests import base
|
||||
from tempest_lib.tests import fake_auth_provider
|
||||
from tempest_lib.tests import fake_http
|
||||
|
||||
|
||||
class BaseRestClientTestClass(base.TestCase):
|
||||
|
||||
url = 'fake_endpoint'
|
||||
|
||||
def setUp(self):
|
||||
super(BaseRestClientTestClass, self).setUp()
|
||||
self.rest_client = rest_client.RestClient(
|
||||
fake_auth_provider.FakeAuthProvider(), None, None)
|
||||
self.stubs.Set(httplib2.Http, 'request', self.fake_http.request)
|
||||
self.useFixture(mockpatch.PatchObject(self.rest_client,
|
||||
'_log_request'))
|
||||
|
||||
|
||||
class TestRestClientHTTPMethods(BaseRestClientTestClass):
|
||||
def setUp(self):
|
||||
self.fake_http = fake_http.fake_httplib2()
|
||||
super(TestRestClientHTTPMethods, self).setUp()
|
||||
self.useFixture(mockpatch.PatchObject(self.rest_client,
|
||||
'_error_checker'))
|
||||
|
||||
def test_post(self):
|
||||
__, return_dict = self.rest_client.post(self.url, {}, {})
|
||||
self.assertEqual('POST', return_dict['method'])
|
||||
|
||||
def test_get(self):
|
||||
__, return_dict = self.rest_client.get(self.url)
|
||||
self.assertEqual('GET', return_dict['method'])
|
||||
|
||||
def test_delete(self):
|
||||
__, return_dict = self.rest_client.delete(self.url)
|
||||
self.assertEqual('DELETE', return_dict['method'])
|
||||
|
||||
def test_patch(self):
|
||||
__, return_dict = self.rest_client.patch(self.url, {}, {})
|
||||
self.assertEqual('PATCH', return_dict['method'])
|
||||
|
||||
def test_put(self):
|
||||
__, return_dict = self.rest_client.put(self.url, {}, {})
|
||||
self.assertEqual('PUT', return_dict['method'])
|
||||
|
||||
def test_head(self):
|
||||
self.useFixture(mockpatch.PatchObject(self.rest_client,
|
||||
'response_checker'))
|
||||
__, return_dict = self.rest_client.head(self.url)
|
||||
self.assertEqual('HEAD', return_dict['method'])
|
||||
|
||||
def test_copy(self):
|
||||
__, return_dict = self.rest_client.copy(self.url)
|
||||
self.assertEqual('COPY', return_dict['method'])
|
||||
|
||||
|
||||
class TestRestClientNotFoundHandling(BaseRestClientTestClass):
|
||||
def setUp(self):
|
||||
self.fake_http = fake_http.fake_httplib2(404)
|
||||
super(TestRestClientNotFoundHandling, self).setUp()
|
||||
|
||||
def test_post(self):
|
||||
self.assertRaises(exceptions.NotFound, self.rest_client.post,
|
||||
self.url, {}, {})
|
||||
|
||||
|
||||
class TestRestClientHeadersJSON(TestRestClientHTTPMethods):
|
||||
TYPE = "json"
|
||||
|
||||
def _verify_headers(self, resp):
|
||||
self.assertEqual(self.rest_client._get_type(), self.TYPE)
|
||||
resp = dict((k.lower(), v) for k, v in six.iteritems(resp))
|
||||
self.assertEqual(self.header_value, resp['accept'])
|
||||
self.assertEqual(self.header_value, resp['content-type'])
|
||||
|
||||
def setUp(self):
|
||||
super(TestRestClientHeadersJSON, self).setUp()
|
||||
self.rest_client.TYPE = self.TYPE
|
||||
self.header_value = 'application/%s' % self.rest_client._get_type()
|
||||
|
||||
def test_post(self):
|
||||
resp, __ = self.rest_client.post(self.url, {})
|
||||
self._verify_headers(resp)
|
||||
|
||||
def test_get(self):
|
||||
resp, __ = self.rest_client.get(self.url)
|
||||
self._verify_headers(resp)
|
||||
|
||||
def test_delete(self):
|
||||
resp, __ = self.rest_client.delete(self.url)
|
||||
self._verify_headers(resp)
|
||||
|
||||
def test_patch(self):
|
||||
resp, __ = self.rest_client.patch(self.url, {})
|
||||
self._verify_headers(resp)
|
||||
|
||||
def test_put(self):
|
||||
resp, __ = self.rest_client.put(self.url, {})
|
||||
self._verify_headers(resp)
|
||||
|
||||
def test_head(self):
|
||||
self.useFixture(mockpatch.PatchObject(self.rest_client,
|
||||
'response_checker'))
|
||||
resp, __ = self.rest_client.head(self.url)
|
||||
self._verify_headers(resp)
|
||||
|
||||
def test_copy(self):
|
||||
resp, __ = self.rest_client.copy(self.url)
|
||||
self._verify_headers(resp)
|
||||
|
||||
|
||||
class TestRestClientUpdateHeaders(BaseRestClientTestClass):
|
||||
def setUp(self):
|
||||
self.fake_http = fake_http.fake_httplib2()
|
||||
super(TestRestClientUpdateHeaders, self).setUp()
|
||||
self.useFixture(mockpatch.PatchObject(self.rest_client,
|
||||
'_error_checker'))
|
||||
self.headers = {'X-Configuration-Session': 'session_id'}
|
||||
|
||||
def test_post_update_headers(self):
|
||||
__, return_dict = self.rest_client.post(self.url, {},
|
||||
extra_headers=True,
|
||||
headers=self.headers)
|
||||
|
||||
self.assertDictContainsSubset(
|
||||
{'X-Configuration-Session': 'session_id',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'},
|
||||
return_dict['headers']
|
||||
)
|
||||
|
||||
def test_get_update_headers(self):
|
||||
__, return_dict = self.rest_client.get(self.url,
|
||||
extra_headers=True,
|
||||
headers=self.headers)
|
||||
|
||||
self.assertDictContainsSubset(
|
||||
{'X-Configuration-Session': 'session_id',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'},
|
||||
return_dict['headers']
|
||||
)
|
||||
|
||||
def test_delete_update_headers(self):
|
||||
__, return_dict = self.rest_client.delete(self.url,
|
||||
extra_headers=True,
|
||||
headers=self.headers)
|
||||
|
||||
self.assertDictContainsSubset(
|
||||
{'X-Configuration-Session': 'session_id',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'},
|
||||
return_dict['headers']
|
||||
)
|
||||
|
||||
def test_patch_update_headers(self):
|
||||
__, return_dict = self.rest_client.patch(self.url, {},
|
||||
extra_headers=True,
|
||||
headers=self.headers)
|
||||
|
||||
self.assertDictContainsSubset(
|
||||
{'X-Configuration-Session': 'session_id',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'},
|
||||
return_dict['headers']
|
||||
)
|
||||
|
||||
def test_put_update_headers(self):
|
||||
__, return_dict = self.rest_client.put(self.url, {},
|
||||
extra_headers=True,
|
||||
headers=self.headers)
|
||||
|
||||
self.assertDictContainsSubset(
|
||||
{'X-Configuration-Session': 'session_id',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'},
|
||||
return_dict['headers']
|
||||
)
|
||||
|
||||
def test_head_update_headers(self):
|
||||
self.useFixture(mockpatch.PatchObject(self.rest_client,
|
||||
'response_checker'))
|
||||
|
||||
__, return_dict = self.rest_client.head(self.url,
|
||||
extra_headers=True,
|
||||
headers=self.headers)
|
||||
|
||||
self.assertDictContainsSubset(
|
||||
{'X-Configuration-Session': 'session_id',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'},
|
||||
return_dict['headers']
|
||||
)
|
||||
|
||||
def test_copy_update_headers(self):
|
||||
__, return_dict = self.rest_client.copy(self.url,
|
||||
extra_headers=True,
|
||||
headers=self.headers)
|
||||
|
||||
self.assertDictContainsSubset(
|
||||
{'X-Configuration-Session': 'session_id',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'},
|
||||
return_dict['headers']
|
||||
)
|
||||
|
||||
|
||||
class TestRestClientParseRespJSON(BaseRestClientTestClass):
|
||||
TYPE = "json"
|
||||
|
||||
keys = ["fake_key1", "fake_key2"]
|
||||
values = ["fake_value1", "fake_value2"]
|
||||
item_expected = dict((key, value) for (key, value) in zip(keys, values))
|
||||
list_expected = {"body_list": [
|
||||
{keys[0]: values[0]},
|
||||
{keys[1]: values[1]},
|
||||
]}
|
||||
dict_expected = {"body_dict": {
|
||||
keys[0]: values[0],
|
||||
keys[1]: values[1],
|
||||
}}
|
||||
|
||||
def setUp(self):
|
||||
self.fake_http = fake_http.fake_httplib2()
|
||||
super(TestRestClientParseRespJSON, self).setUp()
|
||||
self.rest_client.TYPE = self.TYPE
|
||||
|
||||
def test_parse_resp_body_item(self):
|
||||
body = self.rest_client._parse_resp(json.dumps(self.item_expected))
|
||||
self.assertEqual(self.item_expected, body)
|
||||
|
||||
def test_parse_resp_body_list(self):
|
||||
body = self.rest_client._parse_resp(json.dumps(self.list_expected))
|
||||
self.assertEqual(self.list_expected["body_list"], body)
|
||||
|
||||
def test_parse_resp_body_dict(self):
|
||||
body = self.rest_client._parse_resp(json.dumps(self.dict_expected))
|
||||
self.assertEqual(self.dict_expected["body_dict"], body)
|
||||
|
||||
def test_parse_resp_two_top_keys(self):
|
||||
dict_two_keys = self.dict_expected.copy()
|
||||
dict_two_keys.update({"second_key": ""})
|
||||
body = self.rest_client._parse_resp(json.dumps(dict_two_keys))
|
||||
self.assertEqual(dict_two_keys, body)
|
||||
|
||||
def test_parse_resp_one_top_key_without_list_or_dict(self):
|
||||
data = {"one_top_key": "not_list_or_dict_value"}
|
||||
body = self.rest_client._parse_resp(json.dumps(data))
|
||||
self.assertEqual(data, body)
|
||||
|
||||
|
||||
class TestRestClientErrorCheckerJSON(base.TestCase):
|
||||
c_type = "application/json"
|
||||
|
||||
def set_data(self, r_code, enc=None, r_body=None):
|
||||
if enc is None:
|
||||
enc = self.c_type
|
||||
resp_dict = {'status': r_code, 'content-type': enc}
|
||||
resp = httplib2.Response(resp_dict)
|
||||
data = {
|
||||
"method": "fake_method",
|
||||
"url": "fake_url",
|
||||
"headers": "fake_headers",
|
||||
"body": "fake_body",
|
||||
"resp": resp,
|
||||
"resp_body": '{"resp_body": "fake_resp_body"}',
|
||||
}
|
||||
if r_body is not None:
|
||||
data.update({"resp_body": r_body})
|
||||
return data
|
||||
|
||||
def setUp(self):
|
||||
super(TestRestClientErrorCheckerJSON, self).setUp()
|
||||
self.rest_client = rest_client.RestClient(
|
||||
fake_auth_provider.FakeAuthProvider(), None, None)
|
||||
|
||||
def test_response_less_than_400(self):
|
||||
self.rest_client._error_checker(**self.set_data("399"))
|
||||
|
||||
def test_response_400(self):
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
self.rest_client._error_checker,
|
||||
**self.set_data("400"))
|
||||
|
||||
def test_response_401(self):
|
||||
self.assertRaises(exceptions.Unauthorized,
|
||||
self.rest_client._error_checker,
|
||||
**self.set_data("401"))
|
||||
|
||||
def test_response_403(self):
|
||||
self.assertRaises(exceptions.Unauthorized,
|
||||
self.rest_client._error_checker,
|
||||
**self.set_data("403"))
|
||||
|
||||
def test_response_404(self):
|
||||
self.assertRaises(exceptions.NotFound,
|
||||
self.rest_client._error_checker,
|
||||
**self.set_data("404"))
|
||||
|
||||
def test_response_409(self):
|
||||
self.assertRaises(exceptions.Conflict,
|
||||
self.rest_client._error_checker,
|
||||
**self.set_data("409"))
|
||||
|
||||
def test_response_413(self):
|
||||
self.assertRaises(exceptions.OverLimit,
|
||||
self.rest_client._error_checker,
|
||||
**self.set_data("413"))
|
||||
|
||||
def test_response_415(self):
|
||||
self.assertRaises(exceptions.InvalidContentType,
|
||||
self.rest_client._error_checker,
|
||||
**self.set_data("415"))
|
||||
|
||||
def test_response_422(self):
|
||||
self.assertRaises(exceptions.UnprocessableEntity,
|
||||
self.rest_client._error_checker,
|
||||
**self.set_data("422"))
|
||||
|
||||
def test_response_500_with_text(self):
|
||||
# _parse_resp is expected to return 'str'
|
||||
self.assertRaises(exceptions.ServerFault,
|
||||
self.rest_client._error_checker,
|
||||
**self.set_data("500"))
|
||||
|
||||
def test_response_501_with_text(self):
|
||||
self.assertRaises(exceptions.NotImplemented,
|
||||
self.rest_client._error_checker,
|
||||
**self.set_data("501"))
|
||||
|
||||
def test_response_500_with_dict(self):
|
||||
r_body = '{"resp_body": {"err": "fake_resp_body"}}'
|
||||
self.assertRaises(exceptions.ServerFault,
|
||||
self.rest_client._error_checker,
|
||||
**self.set_data("500", r_body=r_body))
|
||||
|
||||
def test_response_501_with_dict(self):
|
||||
r_body = '{"resp_body": {"err": "fake_resp_body"}}'
|
||||
self.assertRaises(exceptions.NotImplemented,
|
||||
self.rest_client._error_checker,
|
||||
**self.set_data("501", r_body=r_body))
|
||||
|
||||
def test_response_bigger_than_400(self):
|
||||
# Any response code, that bigger than 400, and not in
|
||||
# (401, 403, 404, 409, 413, 422, 500, 501)
|
||||
self.assertRaises(exceptions.UnexpectedResponseCode,
|
||||
self.rest_client._error_checker,
|
||||
**self.set_data("402"))
|
||||
|
||||
|
||||
class TestRestClientErrorCheckerTEXT(TestRestClientErrorCheckerJSON):
|
||||
c_type = "text/plain"
|
||||
|
||||
def test_fake_content_type(self):
|
||||
# This test is required only in one exemplar
|
||||
# Any response code, that bigger than 400, and not in
|
||||
# (401, 403, 404, 409, 413, 422, 500, 501)
|
||||
self.assertRaises(exceptions.InvalidContentType,
|
||||
self.rest_client._error_checker,
|
||||
**self.set_data("405", enc="fake_enc"))
|
||||
|
||||
|
||||
class TestRestClientUtils(BaseRestClientTestClass):
|
||||
|
||||
def _is_resource_deleted(self, resource_id):
|
||||
if not isinstance(self.retry_pass, int):
|
||||
return False
|
||||
if self.retry_count >= self.retry_pass:
|
||||
return True
|
||||
self.retry_count = self.retry_count + 1
|
||||
return False
|
||||
|
||||
def setUp(self):
|
||||
self.fake_http = fake_http.fake_httplib2()
|
||||
super(TestRestClientUtils, self).setUp()
|
||||
self.retry_count = 0
|
||||
self.retry_pass = None
|
||||
self.original_deleted_method = self.rest_client.is_resource_deleted
|
||||
self.rest_client.is_resource_deleted = self._is_resource_deleted
|
||||
|
||||
def test_wait_for_resource_deletion(self):
|
||||
self.retry_pass = 2
|
||||
# Ensure timeout long enough for loop execution to hit retry count
|
||||
self.rest_client.build_timeout = 500
|
||||
sleep_mock = self.patch('time.sleep')
|
||||
self.rest_client.wait_for_resource_deletion('1234')
|
||||
self.assertEqual(len(sleep_mock.mock_calls), 2)
|
||||
|
||||
def test_wait_for_resource_deletion_not_deleted(self):
|
||||
self.patch('time.sleep')
|
||||
# Set timeout to be very quick to force exception faster
|
||||
self.rest_client.build_timeout = 1
|
||||
self.assertRaises(exceptions.TimeoutException,
|
||||
self.rest_client.wait_for_resource_deletion,
|
||||
'1234')
|
||||
|
||||
def test_wait_for_deletion_with_unimplemented_deleted_method(self):
|
||||
self.rest_client.is_resource_deleted = self.original_deleted_method
|
||||
self.assertRaises(NotImplementedError,
|
||||
self.rest_client.wait_for_resource_deletion,
|
||||
'1234')
|
||||
|
||||
|
||||
class TestExpectedSuccess(BaseRestClientTestClass):
|
||||
|
||||
def setUp(self):
|
||||
self.fake_http = fake_http.fake_httplib2()
|
||||
super(TestExpectedSuccess, self).setUp()
|
||||
|
||||
def test_expected_succes_int_match(self):
|
||||
expected_code = 202
|
||||
read_code = 202
|
||||
resp = self.rest_client.expected_success(expected_code, read_code)
|
||||
# Assert None resp on success
|
||||
self.assertFalse(resp)
|
||||
|
||||
def test_expected_succes_int_no_match(self):
|
||||
expected_code = 204
|
||||
read_code = 202
|
||||
self.assertRaises(exceptions.InvalidHttpSuccessCode,
|
||||
self.rest_client.expected_success,
|
||||
expected_code, read_code)
|
||||
|
||||
def test_expected_succes_list_match(self):
|
||||
expected_code = [202, 204]
|
||||
read_code = 202
|
||||
resp = self.rest_client.expected_success(expected_code, read_code)
|
||||
# Assert None resp on success
|
||||
self.assertFalse(resp)
|
||||
|
||||
def test_expected_succes_list_no_match(self):
|
||||
expected_code = [202, 204]
|
||||
read_code = 200
|
||||
self.assertRaises(exceptions.InvalidHttpSuccessCode,
|
||||
self.rest_client.expected_success,
|
||||
expected_code, read_code)
|
||||
|
||||
def test_non_success_expected_int(self):
|
||||
expected_code = 404
|
||||
read_code = 202
|
||||
self.assertRaises(AssertionError, self.rest_client.expected_success,
|
||||
expected_code, read_code)
|
||||
|
||||
def test_non_success_expected_list(self):
|
||||
expected_code = [404, 202]
|
||||
read_code = 202
|
||||
self.assertRaises(AssertionError, self.rest_client.expected_success,
|
||||
expected_code, read_code)
|
@ -8,6 +8,7 @@ discover
|
||||
python-subunit>=0.0.18
|
||||
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
|
||||
oslosphinx>=2.2.0 # Apache-2.0
|
||||
oslotest>=1.2.0 # Apache-2.0
|
||||
testrepository>=0.0.18
|
||||
testscenarios>=0.4
|
||||
testtools>=0.9.36,!=1.2.0
|
||||
|
Loading…
x
Reference in New Issue
Block a user