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()