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:
Brian Ober 2016-04-12 19:28:04 +00:00
parent bc9e9ed470
commit f2deb18a8a
5 changed files with 172 additions and 28 deletions

View File

@ -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,

View File

@ -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:

View File

@ -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"

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