Merge "Addresses Expect: 100-continue client behavior"
diff --git a/tempest/api/object_storage/test_object_services.py b/tempest/api/object_storage/test_object_services.py
index e8b035b..dc926e0 100644
--- a/tempest/api/object_storage/test_object_services.py
+++ b/tempest/api/object_storage/test_object_services.py
@@ -179,23 +179,14 @@
@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,
diff --git a/tempest/services/object_storage/object_client.py b/tempest/services/object_storage/object_client.py
index 0acd4ad..fa43d94 100644
--- a/tempest/services/object_storage/object_client.py
+++ b/tempest/services/object_storage/object_client.py
@@ -18,6 +18,7 @@
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 @@
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 @@
: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:
diff --git a/tempest/tests/fake_auth_provider.py b/tempest/tests/fake_auth_provider.py
index bc68d26..769f6a6 100644
--- a/tempest/tests/fake_auth_provider.py
+++ b/tempest/tests/fake_auth_provider.py
@@ -18,3 +18,9 @@
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"
diff --git a/tempest/tests/services/object_storage/__init__.py b/tempest/tests/services/object_storage/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/tests/services/object_storage/__init__.py
diff --git a/tempest/tests/services/object_storage/test_object_client.py b/tempest/tests/services/object_storage/test_object_client.py
new file mode 100644
index 0000000..cd8c8f1
--- /dev/null
+++ b/tempest/tests/services/object_storage/test_object_client.py
@@ -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()