Addresses Expect: 100-continue client behavior
Updates to the object storage object services cases with Expect: 100-continue. The current test case sends data along with the Expect: 100-continue header, which does not properly mimic expected behavior. Updated the method to send no body with the proper content length along with the Expect header then sending the body once the 100-continue is received. Closes-Bug: #1573859 Change-Id: I91b486e067a0acacf7ca121e6d5da006554b5348
This commit is contained in:
parent
bc9e9ed470
commit
f2deb18a8a
@ -179,23 +179,14 @@ class ObjectTest(base.BaseObjectTest):
|
||||
@test.idempotent_id('84dafe57-9666-4f6d-84c8-0814d37923b8')
|
||||
def test_create_object_with_expect_continue(self):
|
||||
# create object with expect_continue
|
||||
|
||||
object_name = data_utils.rand_name(name='TestObject')
|
||||
data = data_utils.arbitrary_string()
|
||||
metadata = {'Expect': '100-continue'}
|
||||
resp = self.object_client.create_object_continue(
|
||||
self.container_name,
|
||||
object_name,
|
||||
data,
|
||||
metadata=metadata)
|
||||
|
||||
self.assertIn('status', resp)
|
||||
self.assertEqual(resp['status'], '100')
|
||||
status, _ = self.object_client.create_object_continue(
|
||||
self.container_name, object_name, data)
|
||||
|
||||
self.object_client.create_object_continue(
|
||||
self.container_name,
|
||||
object_name,
|
||||
data,
|
||||
metadata=None)
|
||||
self.assertEqual(status, 201)
|
||||
|
||||
# check uploaded content
|
||||
_, body = self.object_client.get_object(self.container_name,
|
||||
|
@ -18,6 +18,7 @@ from six.moves import http_client as httplib
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
from tempest.lib.common import rest_client
|
||||
from tempest.lib import exceptions
|
||||
|
||||
|
||||
class ObjectClient(rest_client.RestClient):
|
||||
@ -170,29 +171,67 @@ class ObjectClient(rest_client.RestClient):
|
||||
|
||||
def create_object_continue(self, container, object_name,
|
||||
data, metadata=None):
|
||||
"""Create storage object."""
|
||||
"""Put an object using Expect:100-continue"""
|
||||
headers = {}
|
||||
if metadata:
|
||||
for key in metadata:
|
||||
headers[str(key)] = metadata[key]
|
||||
|
||||
if not data:
|
||||
headers['content-length'] = '0'
|
||||
|
||||
headers['X-Auth-Token'] = self.token
|
||||
headers['content-length'] = 0 if data is None else len(data)
|
||||
headers['Expect'] = '100-continue'
|
||||
|
||||
conn = put_object_connection(self.base_url, str(container),
|
||||
str(object_name), data, None, headers)
|
||||
parsed = urlparse.urlparse(self.base_url)
|
||||
path = str(parsed.path) + "/"
|
||||
path += "%s/%s" % (str(container), str(object_name))
|
||||
|
||||
conn = create_connection(parsed)
|
||||
|
||||
# Send the PUT request and the headers including the "Expect" header
|
||||
conn.putrequest('PUT', path)
|
||||
|
||||
for header, value in six.iteritems(headers):
|
||||
conn.putheader(header, value)
|
||||
conn.endheaders()
|
||||
|
||||
# Read the 100 status prior to sending the data
|
||||
response = conn.response_class(conn.sock,
|
||||
strict=conn.strict,
|
||||
method=conn._method)
|
||||
version, status, reason = response._read_status()
|
||||
resp = {'version': version,
|
||||
'status': str(status),
|
||||
'reason': reason}
|
||||
_, status, _ = response._read_status()
|
||||
|
||||
return resp
|
||||
# toss the CRLF at the end of the response
|
||||
response._safe_read(2)
|
||||
|
||||
# Expecting a 100 here, if not close and throw an exception
|
||||
if status != 100:
|
||||
conn.close()
|
||||
pattern = "%s %s" % (
|
||||
"""Unexpected http success status code {0}.""",
|
||||
"""The expected status code is {1}""")
|
||||
details = pattern.format(status, 100)
|
||||
raise exceptions.UnexpectedResponseCode(details)
|
||||
|
||||
# If a continue was received go ahead and send the data
|
||||
# and get the final response
|
||||
conn.send(data)
|
||||
|
||||
resp = conn.getresponse()
|
||||
|
||||
return resp.status, resp.reason
|
||||
|
||||
|
||||
def create_connection(parsed_url):
|
||||
"""Helper function to create connection with httplib
|
||||
|
||||
:param parsed_url: parsed url of the remote location
|
||||
"""
|
||||
if parsed_url.scheme == 'https':
|
||||
conn = httplib.HTTPSConnection(parsed_url.netloc)
|
||||
else:
|
||||
conn = httplib.HTTPConnection(parsed_url.netloc)
|
||||
|
||||
return conn
|
||||
|
||||
|
||||
def put_object_connection(base_url, container, name, contents=None,
|
||||
@ -211,13 +250,12 @@ def put_object_connection(base_url, container, name, contents=None,
|
||||
:param query_string: if set will be appended with '?' to generated path
|
||||
"""
|
||||
parsed = urlparse.urlparse(base_url)
|
||||
if parsed.scheme == 'https':
|
||||
conn = httplib.HTTPSConnection(parsed.netloc)
|
||||
else:
|
||||
conn = httplib.HTTPConnection(parsed.netloc)
|
||||
|
||||
path = str(parsed.path) + "/"
|
||||
path += "%s/%s" % (str(container), str(name))
|
||||
|
||||
conn = create_connection(parsed)
|
||||
|
||||
if query_string:
|
||||
path += '?' + query_string
|
||||
if headers:
|
||||
|
@ -18,3 +18,9 @@ class FakeAuthProvider(object):
|
||||
|
||||
def auth_request(self, method, url, headers=None, body=None, filters=None):
|
||||
return url, headers, body
|
||||
|
||||
def get_token(self):
|
||||
return "faketoken"
|
||||
|
||||
def base_url(self, filters, auth_data=None):
|
||||
return "https://example.com"
|
||||
|
0
tempest/tests/services/object_storage/__init__.py
Normal file
0
tempest/tests/services/object_storage/__init__.py
Normal file
109
tempest/tests/services/object_storage/test_object_client.py
Normal file
109
tempest/tests/services/object_storage/test_object_client.py
Normal file
@ -0,0 +1,109 @@
|
||||
# Copyright 2016 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 mock
|
||||
import six
|
||||
|
||||
from tempest.lib import exceptions
|
||||
from tempest.services.object_storage import object_client
|
||||
from tempest.tests import base
|
||||
from tempest.tests import fake_auth_provider
|
||||
|
||||
|
||||
class TestObjectClient(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestObjectClient, self).setUp()
|
||||
self.fake_auth = fake_auth_provider.FakeAuthProvider()
|
||||
self.url = self.fake_auth.base_url(None)
|
||||
self.object_client = object_client.ObjectClient(self.fake_auth,
|
||||
'swift', 'region1')
|
||||
|
||||
@mock.patch.object(object_client, 'create_connection')
|
||||
def test_create_object_continue_no_data(self, mock_poc):
|
||||
self._validate_create_object_continue(None, mock_poc)
|
||||
|
||||
@mock.patch.object(object_client, 'create_connection')
|
||||
def test_create_object_continue_with_data(self, mock_poc):
|
||||
self._validate_create_object_continue('hello', mock_poc)
|
||||
|
||||
@mock.patch.object(object_client, 'create_connection')
|
||||
def test_create_continue_with_no_continue_received(self, mock_poc):
|
||||
self._validate_create_object_continue('hello', mock_poc,
|
||||
initial_status=201)
|
||||
|
||||
def _validate_create_object_continue(self, req_data,
|
||||
mock_poc, initial_status=100):
|
||||
|
||||
expected_hdrs = {
|
||||
'X-Auth-Token': self.fake_auth.get_token(),
|
||||
'content-length': 0 if req_data is None else len(req_data),
|
||||
'Expect': '100-continue'}
|
||||
|
||||
# Setup the Mocks prior to invoking the object creation
|
||||
mock_resp_cls = mock.Mock()
|
||||
mock_resp_cls._read_status.return_value = ("1", initial_status, "OK")
|
||||
|
||||
mock_poc.return_value.response_class.return_value = mock_resp_cls
|
||||
|
||||
# This is the final expected return value
|
||||
mock_poc.return_value.getresponse.return_value.status = 201
|
||||
mock_poc.return_value.getresponse.return_value.reason = 'OK'
|
||||
|
||||
# Call method to PUT object using expect:100-continue
|
||||
cnt = "container1"
|
||||
obj = "object1"
|
||||
path = "/%s/%s" % (cnt, obj)
|
||||
|
||||
# If the expected initial status is not 100, then an exception
|
||||
# should be thrown and the connection closed
|
||||
if initial_status is 100:
|
||||
status, reason = \
|
||||
self.object_client.create_object_continue(cnt, obj, req_data)
|
||||
else:
|
||||
self.assertRaises(exceptions.UnexpectedResponseCode,
|
||||
self.object_client.create_object_continue, cnt,
|
||||
obj, req_data)
|
||||
mock_poc.return_value.close.assert_called_once_with()
|
||||
|
||||
# Verify that putrequest is called 1 time with the appropriate values
|
||||
mock_poc.return_value.putrequest.assert_called_once_with('PUT', path)
|
||||
|
||||
# Verify that headers were written, including "Expect:100-continue"
|
||||
calls = []
|
||||
|
||||
for header, value in six.iteritems(expected_hdrs):
|
||||
calls.append(mock.call(header, value))
|
||||
|
||||
mock_poc.return_value.putheader.assert_has_calls(calls, False)
|
||||
mock_poc.return_value.endheaders.assert_called_once_with()
|
||||
|
||||
# The following steps are only taken if the initial status is 100
|
||||
if initial_status is 100:
|
||||
# Verify that the method returned what it was supposed to
|
||||
self.assertEqual(status, 201)
|
||||
|
||||
# Verify that _safe_read was called once to remove the CRLF
|
||||
# after the 100 response
|
||||
mock_rc = mock_poc.return_value.response_class.return_value
|
||||
mock_rc._safe_read.assert_called_once_with(2)
|
||||
|
||||
# Verify the actual data was written via send
|
||||
mock_poc.return_value.send.assert_called_once_with(req_data)
|
||||
|
||||
# Verify that the getresponse method was called to receive
|
||||
# the final
|
||||
mock_poc.return_value.getresponse.assert_called_once_with()
|
Loading…
Reference in New Issue
Block a user