From f2deb18a8a6df8d8ca849705e884133e429f9099 Mon Sep 17 00:00:00 2001 From: Brian Ober Date: Tue, 12 Apr 2016 19:28:04 +0000 Subject: [PATCH] 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 --- .../object_storage/test_object_services.py | 17 +-- .../services/object_storage/object_client.py | 68 ++++++++--- tempest/tests/fake_auth_provider.py | 6 + .../tests/services/object_storage/__init__.py | 0 .../object_storage/test_object_client.py | 109 ++++++++++++++++++ 5 files changed, 172 insertions(+), 28 deletions(-) create mode 100644 tempest/tests/services/object_storage/__init__.py create mode 100644 tempest/tests/services/object_storage/test_object_client.py diff --git a/tempest/api/object_storage/test_object_services.py b/tempest/api/object_storage/test_object_services.py index e8b035bbe7..dc926e0f41 100644 --- a/tempest/api/object_storage/test_object_services.py +++ b/tempest/api/object_storage/test_object_services.py @@ -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, diff --git a/tempest/services/object_storage/object_client.py b/tempest/services/object_storage/object_client.py index 0acd4ad486..fa43d947cd 100644 --- a/tempest/services/object_storage/object_client.py +++ b/tempest/services/object_storage/object_client.py @@ -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: diff --git a/tempest/tests/fake_auth_provider.py b/tempest/tests/fake_auth_provider.py index bc68d2682f..769f6a638c 100644 --- a/tempest/tests/fake_auth_provider.py +++ b/tempest/tests/fake_auth_provider.py @@ -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" diff --git a/tempest/tests/services/object_storage/__init__.py b/tempest/tests/services/object_storage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..cd8c8f1d33 --- /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()