From 1ded0d6c8793ca3eca573c098cef78b5ae41f080 Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Thu, 11 Oct 2018 15:23:39 -0700 Subject: [PATCH] Allow arbitrary UTF-8 strings as delimiters in listings AWS seems to support this, so let's allow s3api to do it, too. Previously, S3 clients trying to use multi-character delimiters would get 500s back, because s3api didn't know how to handle the 412s that the container server would send. As long as we're adding support for container listings, may as well do it for accounts, too. Change-Id: I62032ddd50a3493b8b99a40fb48d840ac763d0e7 Co-Authored-By: Thiago da Silva Closes-Bug: #1797305 --- swift/account/backend.py | 10 +- swift/account/server.py | 3 - swift/container/backend.py | 18 +++- swift/container/server.py | 3 - test/functional/s3api/test_bucket.py | 18 ++++ test/functional/tests.py | 49 ++++++++++ test/probe/test_sharder.py | 14 ++- test/unit/account/test_server.py | 120 ++++++++++++++++++++--- test/unit/container/test_server.py | 138 ++++++++++++++++++++++----- 9 files changed, 314 insertions(+), 59 deletions(-) diff --git a/swift/account/backend.py b/swift/account/backend.py index dddcbce6ef..1e4b5d7018 100644 --- a/swift/account/backend.py +++ b/swift/account/backend.py @@ -457,12 +457,16 @@ class AccountBroker(DatabaseBroker): end = name.find(delimiter, len(prefix)) if end > 0: if reverse: - end_marker = name[:end + 1] + end_marker = name[:end + len(delimiter)] else: - marker = name[:end] + chr(ord(delimiter) + 1) + marker = ''.join([ + name[:end], + delimiter[:-1], + chr(ord(delimiter[-1:]) + 1), + ]) # we want result to be inclusive of delim+1 delim_force_gte = True - dir_name = name[:end + 1] + dir_name = name[:end + len(delimiter)] if dir_name != orig_marker: results.append([dir_name, 0, 0, '0', 1]) curs.close() diff --git a/swift/account/server.py b/swift/account/server.py index 3b076054e6..bda89f82e9 100644 --- a/swift/account/server.py +++ b/swift/account/server.py @@ -207,9 +207,6 @@ class AccountController(BaseStorageServer): drive, part, account = split_and_validate_path(req, 3) prefix = get_param(req, 'prefix') delimiter = get_param(req, 'delimiter') - if delimiter and (len(delimiter) > 1 or ord(delimiter) > 254): - # delimiters can be made more flexible later - return HTTPPreconditionFailed(body='Bad delimiter') limit = constraints.ACCOUNT_LISTING_LIMIT given_limit = get_param(req, 'limit') reverse = config_true_value(get_param(req, 'reverse')) diff --git a/swift/container/backend.py b/swift/container/backend.py index 9407177e7a..8cd671eef3 100644 --- a/swift/container/backend.py +++ b/swift/container/backend.py @@ -1186,19 +1186,27 @@ class ContainerBroker(DatabaseBroker): continue if end >= 0 and len(name) > end + len(delimiter): if reverse: - end_marker = name[:end + 1] + end_marker = name[:end + len(delimiter)] else: - marker = name[:end] + chr(ord(delimiter) + 1) + marker = ''.join([ + name[:end], + delimiter[:-1], + chr(ord(delimiter[-1:]) + 1), + ]) curs.close() break elif end >= 0: if reverse: - end_marker = name[:end + 1] + end_marker = name[:end + len(delimiter)] else: - marker = name[:end] + chr(ord(delimiter) + 1) + marker = ''.join([ + name[:end], + delimiter[:-1], + chr(ord(delimiter[-1:]) + 1), + ]) # we want result to be inclusive of delim+1 delim_force_gte = True - dir_name = name[:end + 1] + dir_name = name[:end + len(delimiter)] if dir_name != orig_marker: results.append([dir_name, '0', 0, None, '']) curs.close() diff --git a/swift/container/server.py b/swift/container/server.py index 0ae348d0a0..0e8a443442 100644 --- a/swift/container/server.py +++ b/swift/container/server.py @@ -637,9 +637,6 @@ class ContainerController(BaseStorageServer): path = get_param(req, 'path') prefix = get_param(req, 'prefix') delimiter = get_param(req, 'delimiter') - if delimiter and (len(delimiter) > 1 or ord(delimiter) > 254): - # delimiters can be made more flexible later - return HTTPPreconditionFailed(body='Bad delimiter') marker = get_param(req, 'marker', '') end_marker = get_param(req, 'end_marker') limit = constraints.CONTAINER_LISTING_LIMIT diff --git a/test/functional/s3api/test_bucket.py b/test/functional/s3api/test_bucket.py index c1f64e6ec6..d246e7baec 100644 --- a/test/functional/s3api/test_bucket.py +++ b/test/functional/s3api/test_bucket.py @@ -235,6 +235,24 @@ class TestS3ApiBucket(S3ApiBaseBoto3): resp_prefixes, [{'Prefix': p} for p in expect_prefixes]) + def test_get_bucket_with_multi_char_delimiter(self): + bucket = 'bucket' + put_objects = ('object', 'object2', 'subdir/object', 'subdir2/object', + 'dir/subdir/object') + self._prepare_test_get_bucket(bucket, put_objects) + + delimiter = '/obj' + expect_objects = ('object', 'object2') + expect_prefixes = ('dir/subdir/obj', 'subdir/obj', 'subdir2/obj') + resp = self.conn.list_objects(Bucket=bucket, Delimiter=delimiter) + self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode']) + self.assertEqual(resp['Delimiter'], delimiter) + self._validate_object_listing(resp['Contents'], expect_objects) + resp_prefixes = resp['CommonPrefixes'] + self.assertEqual( + resp_prefixes, + [{'Prefix': p} for p in expect_prefixes]) + def test_get_bucket_with_encoding_type(self): bucket = 'bucket' put_objects = ('object', 'object2') diff --git a/test/functional/tests.py b/test/functional/tests.py index 4ea01f20c3..6c63cfa0a3 100644 --- a/test/functional/tests.py +++ b/test/functional/tests.py @@ -301,6 +301,25 @@ class TestAccount(Base): results = [r for r in results if r in expected] self.assertEqual(expected, results) + def testListMultiCharDelimiter(self): + delimiter = '-&' + containers = ['test', delimiter.join(['test', 'bar']), + delimiter.join(['test', 'foo'])] + for c in containers: + cont = self.env.account.container(c) + self.assertTrue(cont.create()) + + results = self.env.account.containers(parms={'delimiter': delimiter}) + expected = ['test', 'test-&'] + results = [r for r in results if r in expected] + self.assertEqual(expected, results) + + results = self.env.account.containers(parms={'delimiter': delimiter, + 'reverse': 'yes'}) + expected.reverse() + results = [r for r in results if r in expected] + self.assertEqual(expected, results) + def testListDelimiterAndPrefix(self): delimiter = 'a' containers = ['bar', 'bazar'] @@ -668,6 +687,36 @@ class TestContainer(Base): results = [x.get('name', x.get('subdir')) for x in results] self.assertEqual(results, ['test-', 'test']) + def testListMultiCharDelimiter(self): + cont = self.env.account.container(Utils.create_name()) + self.assertTrue(cont.create()) + + delimiter = '-&' + files = ['test', delimiter.join(['test', 'bar']), + delimiter.join(['test', 'foo'])] + for f in files: + file_item = cont.file(f) + self.assertTrue(file_item.write_random()) + + for format_type in [None, 'json', 'xml']: + results = cont.files(parms={'format': format_type}) + if isinstance(results[0], dict): + results = [x.get('name', x.get('subdir')) for x in results] + self.assertEqual(results, ['test', 'test-&bar', 'test-&foo']) + + results = cont.files(parms={'delimiter': delimiter, + 'format': format_type}) + if isinstance(results[0], dict): + results = [x.get('name', x.get('subdir')) for x in results] + self.assertEqual(results, ['test', 'test-&']) + + results = cont.files(parms={'delimiter': delimiter, + 'format': format_type, + 'reverse': 'yes'}) + if isinstance(results[0], dict): + results = [x.get('name', x.get('subdir')) for x in results] + self.assertEqual(results, ['test-&', 'test']) + def testListDelimiterAndPrefix(self): cont = self.env.account.container(Utils.create_name()) self.assertTrue(cont.create()) diff --git a/test/probe/test_sharder.py b/test/probe/test_sharder.py index b49eeee44c..6d4fadc7e9 100644 --- a/test/probe/test_sharder.py +++ b/test/probe/test_sharder.py @@ -432,12 +432,12 @@ class TestContainerShardingNonUTF8(BaseTestContainerSharding): expected = expected[:params['limit']] self.assertEqual(expected, listing) - def check_listing_precondition_fails(**params): + def check_listing_fails(exp_status, **params): qs = '&'.join(['%s=%s' % param for param in params.items()]) with self.assertRaises(ClientException) as cm: client.get_container( self.url, self.token, self.container_name, query_string=qs) - self.assertEqual(412, cm.exception.http_status) + self.assertEqual(exp_status, cm.exception.http_status) return cm.exception def do_listing_checks(objects): @@ -469,12 +469,16 @@ class TestContainerShardingNonUTF8(BaseTestContainerSharding): self.url, self.token, self.container_name, query_string='delimiter=-') self.assertEqual([{'subdir': 'obj-'}], listing) + headers, listing = client.get_container( + self.url, self.token, self.container_name, + query_string='delimiter=j-') + self.assertEqual([{'subdir': 'obj-'}], listing) limit = self.cluster_info['swift']['container_listing_limit'] - exc = check_listing_precondition_fails(limit=limit + 1) + exc = check_listing_fails(412, limit=limit + 1) self.assertIn(b'Maximum limit', exc.http_response_content) - exc = check_listing_precondition_fails(delimiter='ab') - self.assertIn(b'Bad delimiter', exc.http_response_content) + exc = check_listing_fails(400, delimiter='%ff') + self.assertIn(b'not valid UTF-8', exc.http_response_content) # sanity checks do_listing_checks(obj_names) diff --git a/test/unit/account/test_server.py b/test/unit/account/test_server.py index 31359140c6..7a2341d786 100644 --- a/test/unit/account/test_server.py +++ b/test/unit/account/test_server.py @@ -1485,13 +1485,6 @@ class TestAccountController(unittest.TestCase): resp = req.get_response(self.controller) self.assertEqual(resp.status_int, 406) - def test_GET_delimiter_too_long(self): - req = Request.blank('/sda1/p/a?delimiter=xx', - environ={'REQUEST_METHOD': 'GET', - 'HTTP_X_TIMESTAMP': '0'}) - resp = req.get_response(self.controller) - self.assertEqual(resp.status_int, 412) - def test_GET_prefix_delimiter_plain(self): req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT', 'HTTP_X_TIMESTAMP': '0'}) @@ -1655,6 +1648,110 @@ class TestAccountController(unittest.TestCase): listing.append(node2.firstChild.nodeValue) self.assertEqual(listing, ['sub.1.0', 'sub.1.1', 'sub.1.2']) + def test_GET_multichar_delimiter(self): + self.maxDiff = None + req = Request.blank('/sda1/p/a', method='PUT', headers={ + 'x-timestamp': '0'}) + resp = req.get_response(self.controller) + self.assertEqual(resp.status_int, 201, resp.body) + for i in ('US~~TX~~A', 'US~~TX~~B', 'US~~OK~~A', 'US~~OK~~B', + 'US~~OK~Tulsa~~A', 'US~~OK~Tulsa~~B', + 'US~~UT~~A', 'US~~UT~~~B'): + req = Request.blank('/sda1/p/a/%s' % i, method='PUT', headers={ + 'X-Put-Timestamp': '1', + 'X-Delete-Timestamp': '0', + 'X-Object-Count': '0', + 'X-Bytes-Used': '0', + 'X-Timestamp': normalize_timestamp(0)}) + resp = req.get_response(self.controller) + self.assertEqual(resp.status_int, 201) + req = Request.blank( + '/sda1/p/a?prefix=US~~&delimiter=~~&format=json', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + json.loads(resp.body), + [{"subdir": "US~~OK~Tulsa~~"}, + {"subdir": "US~~OK~~"}, + {"subdir": "US~~TX~~"}, + {"subdir": "US~~UT~~"}]) + + req = Request.blank( + '/sda1/p/a?prefix=US~~&delimiter=~~&format=json&reverse=on', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + json.loads(resp.body), + [{"subdir": "US~~UT~~"}, + {"subdir": "US~~TX~~"}, + {"subdir": "US~~OK~~"}, + {"subdir": "US~~OK~Tulsa~~"}]) + + req = Request.blank( + '/sda1/p/a?prefix=US~~UT&delimiter=~~&format=json', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + json.loads(resp.body), + [{"subdir": "US~~UT~~"}]) + + req = Request.blank( + '/sda1/p/a?prefix=US~~UT&delimiter=~~&format=json&reverse=on', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + json.loads(resp.body), + [{"subdir": "US~~UT~~"}]) + + req = Request.blank( + '/sda1/p/a?prefix=US~~UT~&delimiter=~~&format=json', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + [{k: v for k, v in item.items() if k in ('subdir', 'name')} + for item in json.loads(resp.body)], + [{"name": "US~~UT~~A"}, + {"subdir": "US~~UT~~~"}]) + + req = Request.blank( + '/sda1/p/a?prefix=US~~UT~&delimiter=~~&format=json&reverse=on', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + [{k: v for k, v in item.items() if k in ('subdir', 'name')} + for item in json.loads(resp.body)], + [{"subdir": "US~~UT~~~"}, + {"name": "US~~UT~~A"}]) + + req = Request.blank( + '/sda1/p/a?prefix=US~~UT~~&delimiter=~~&format=json', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + [{k: v for k, v in item.items() if k in ('subdir', 'name')} + for item in json.loads(resp.body)], + [{"name": "US~~UT~~A"}, + {"name": "US~~UT~~~B"}]) + + req = Request.blank( + '/sda1/p/a?prefix=US~~UT~~&delimiter=~~&format=json&reverse=on', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + [{k: v for k, v in item.items() if k in ('subdir', 'name')} + for item in json.loads(resp.body)], + [{"name": "US~~UT~~~B"}, + {"name": "US~~UT~~A"}]) + + req = Request.blank( + '/sda1/p/a?prefix=US~~UT~~~&delimiter=~~&format=json', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + [{k: v for k, v in item.items() if k in ('subdir', 'name')} + for item in json.loads(resp.body)], + [{"name": "US~~UT~~~B"}]) + def test_through_call(self): inbuf = BytesIO() errbuf = StringIO() @@ -1779,18 +1876,13 @@ class TestAccountController(unittest.TestCase): resp = req.get_response(self.controller) self.assertEqual(resp.status_int, 400, "%d on param %s" % (resp.status_int, param)) - # Good UTF8 sequence for delimiter, too long (1 byte delimiters only) - req = Request.blank('/sda1/p/a?delimiter=\xce\xa9', - environ={'REQUEST_METHOD': 'GET'}) - resp = req.get_response(self.controller) - self.assertEqual(resp.status_int, 412, - "%d on param delimiter" % (resp.status_int)) Request.blank('/sda1/p/a', headers={'X-Timestamp': normalize_timestamp(1)}, environ={'REQUEST_METHOD': 'PUT'}).get_response( self.controller) # Good UTF8 sequence, ignored for limit, doesn't affect other queries - for param in ('limit', 'marker', 'prefix', 'end_marker', 'format'): + for param in ('limit', 'marker', 'prefix', 'end_marker', 'format', + 'delimiter'): req = Request.blank('/sda1/p/a?%s=\xce\xa9' % param, environ={'REQUEST_METHOD': 'GET'}) resp = req.get_response(self.controller) diff --git a/test/unit/container/test_server.py b/test/unit/container/test_server.py index c45d4bb7c2..50cf2eaaf4 100644 --- a/test/unit/container/test_server.py +++ b/test/unit/container/test_server.py @@ -3028,13 +3028,14 @@ class TestContainerController(unittest.TestCase): '/sda1/p/a/c', method='PUT', headers=headers, body=body) self.assertEqual(202, req.get_response(self.controller).status_int) - def do_test(params): + def do_test(params, expected_status): params['format'] = 'json' headers = {'X-Backend-Record-Type': 'shard'} req = Request.blank('/sda1/p/a/c', method='GET', headers=headers, params=params) with mock_timestamp_now(ts_now): resp = req.get_response(self.controller) + self.assertEqual(resp.status_int, expected_status) self.assertEqual(resp.content_type, 'text/html') self.assertNotIn('X-Backend-Record-Type', resp.headers) self.assertNotIn('X-Backend-Sharding-State', resp.headers) @@ -3042,26 +3043,19 @@ class TestContainerController(unittest.TestCase): self.assertNotIn('X-Container-Bytes-Used', resp.headers) self.assertNotIn('X-Timestamp', resp.headers) self.assertNotIn('X-PUT-Timestamp', resp.headers) - return resp - resp = do_test({'states': 'bad'}) - self.assertEqual(resp.status_int, 400) - resp = do_test({'delimiter': 'bad'}) - self.assertEqual(resp.status_int, 412) - resp = do_test({'limit': str(constraints.CONTAINER_LISTING_LIMIT + 1)}) - self.assertEqual(resp.status_int, 412) + do_test({'states': 'bad'}, 400) + do_test({'limit': str(constraints.CONTAINER_LISTING_LIMIT + 1)}, 412) with mock.patch('swift.container.server.check_drive', side_effect=ValueError('sda1 is not mounted')): - resp = do_test({}) - self.assertEqual(resp.status_int, 507) + do_test({}, 507) # delete the container req = Request.blank('/sda1/p/a/c', method='DELETE', headers={'X-Timestamp': next(ts_iter).normal}) self.assertEqual(204, req.get_response(self.controller).status_int) - resp = do_test({'states': 'bad'}) - self.assertEqual(resp.status_int, 404) + do_test({'states': 'bad'}, 404) def test_GET_auto_record_type(self): # make a container @@ -3982,13 +3976,6 @@ class TestContainerController(unittest.TestCase): resp = req.get_response(self.controller) self.assertEqual(resp.body.split(b'\n'), [b'a1', b'a2', b'a3', b'']) - def test_GET_delimiter_too_long(self): - req = Request.blank('/sda1/p/a/c?delimiter=xx', - environ={'REQUEST_METHOD': 'GET', - 'HTTP_X_TIMESTAMP': '0'}) - resp = req.get_response(self.controller) - self.assertEqual(resp.status_int, 412) - def test_GET_delimiter(self): req = Request.blank( '/sda1/p/a/c', environ={'REQUEST_METHOD': 'PUT', @@ -4014,6 +4001,111 @@ class TestContainerController(unittest.TestCase): {"subdir": "US-TX-"}, {"subdir": "US-UT-"}]) + def test_GET_multichar_delimiter(self): + self.maxDiff = None + req = Request.blank( + '/sda1/p/a/c', environ={'REQUEST_METHOD': 'PUT', + 'HTTP_X_TIMESTAMP': '0'}) + resp = req.get_response(self.controller) + for i in ('US~~TX~~A', 'US~~TX~~B', 'US~~OK~~A', 'US~~OK~~B', + 'US~~OK~Tulsa~~A', 'US~~OK~Tulsa~~B', + 'US~~UT~~A', 'US~~UT~~~B'): + req = Request.blank( + '/sda1/p/a/c/%s' % i, + environ={ + 'REQUEST_METHOD': 'PUT', 'HTTP_X_TIMESTAMP': '1', + 'HTTP_X_CONTENT_TYPE': 'text/plain', 'HTTP_X_ETAG': 'x', + 'HTTP_X_SIZE': 0}) + self._update_object_put_headers(req) + resp = req.get_response(self.controller) + self.assertEqual(resp.status_int, 201) + req = Request.blank( + '/sda1/p/a/c?prefix=US~~&delimiter=~~&format=json', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + json.loads(resp.body), + [{"subdir": "US~~OK~Tulsa~~"}, + {"subdir": "US~~OK~~"}, + {"subdir": "US~~TX~~"}, + {"subdir": "US~~UT~~"}]) + + req = Request.blank( + '/sda1/p/a/c?prefix=US~~&delimiter=~~&format=json&reverse=on', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + json.loads(resp.body), + [{"subdir": "US~~UT~~"}, + {"subdir": "US~~TX~~"}, + {"subdir": "US~~OK~~"}, + {"subdir": "US~~OK~Tulsa~~"}]) + + req = Request.blank( + '/sda1/p/a/c?prefix=US~~UT&delimiter=~~&format=json', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + json.loads(resp.body), + [{"subdir": "US~~UT~~"}]) + + req = Request.blank( + '/sda1/p/a/c?prefix=US~~UT&delimiter=~~&format=json&reverse=on', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + json.loads(resp.body), + [{"subdir": "US~~UT~~"}]) + + req = Request.blank( + '/sda1/p/a/c?prefix=US~~UT~&delimiter=~~&format=json', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + [{k: v for k, v in item.items() if k in ('subdir', 'name')} + for item in json.loads(resp.body)], + [{"name": "US~~UT~~A"}, + {"subdir": "US~~UT~~~"}]) + + req = Request.blank( + '/sda1/p/a/c?prefix=US~~UT~&delimiter=~~&format=json&reverse=on', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + [{k: v for k, v in item.items() if k in ('subdir', 'name')} + for item in json.loads(resp.body)], + [{"subdir": "US~~UT~~~"}, + {"name": "US~~UT~~A"}]) + + req = Request.blank( + '/sda1/p/a/c?prefix=US~~UT~~&delimiter=~~&format=json', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + [{k: v for k, v in item.items() if k in ('subdir', 'name')} + for item in json.loads(resp.body)], + [{"name": "US~~UT~~A"}, + {"name": "US~~UT~~~B"}]) + + req = Request.blank( + '/sda1/p/a/c?prefix=US~~UT~~&delimiter=~~&format=json&reverse=on', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + [{k: v for k, v in item.items() if k in ('subdir', 'name')} + for item in json.loads(resp.body)], + [{"name": "US~~UT~~~B"}, + {"name": "US~~UT~~A"}]) + + req = Request.blank( + '/sda1/p/a/c?prefix=US~~UT~~~&delimiter=~~&format=json', + environ={'REQUEST_METHOD': 'GET'}) + resp = req.get_response(self.controller) + self.assertEqual( + [{k: v for k, v in item.items() if k in ('subdir', 'name')} + for item in json.loads(resp.body)], + [{"name": "US~~UT~~~B"}]) + def test_GET_delimiter_non_ascii(self): req = Request.blank( '/sda1/p/a/c', environ={'REQUEST_METHOD': 'PUT', @@ -4279,18 +4371,12 @@ class TestContainerController(unittest.TestCase): resp = req.get_response(self.controller) self.assertEqual(resp.status_int, 400, "%d on param %s" % (resp.status_int, param)) - # Good UTF8 sequence for delimiter, too long (1 byte delimiters only) - req = Request.blank('/sda1/p/a/c?delimiter=\xce\xa9', - environ={'REQUEST_METHOD': 'GET'}) - resp = req.get_response(self.controller) - self.assertEqual(resp.status_int, 412, - "%d on param delimiter" % (resp.status_int)) req = Request.blank('/sda1/p/a/c', method='PUT', headers={'X-Timestamp': Timestamp(1).internal}) req.get_response(self.controller) # Good UTF8 sequence, ignored for limit, doesn't affect other queries for param in ('limit', 'marker', 'path', 'prefix', 'end_marker', - 'format'): + 'format', 'delimiter'): req = Request.blank('/sda1/p/a/c?%s=\xce\xa9' % param, environ={'REQUEST_METHOD': 'GET'}) resp = req.get_response(self.controller)