diff --git a/requirements.txt b/requirements.txt index 7f1348a..beced13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ requests ==0.14.0 pytz >=2011k ordereddict httplib2 +lxml diff --git a/s3tests/common.py b/s3tests/common.py index a9ab28e..7b5cb47 100644 --- a/s3tests/common.py +++ b/s3tests/common.py @@ -5,6 +5,7 @@ import os import random import string import yaml +from lxml import etree s3 = bunch.Bunch() config = bunch.Bunch() @@ -251,3 +252,19 @@ def with_setup_kwargs(setup, teardown=None): #def test_gen(): # yield _test_gen, '1' # yield _test_gen + + +def normalize_xml_whitespace(xml, pretty_print=True): + root = etree.fromstring(xml.encode(encoding='ascii')) + + for element in root.iter('*'): + if element.text is not None and not element.text.strip(): + element.text = None + if element.text is not None: + element.text = element.text.strip().replace("\n","").replace("\r","") + if element.tail is not None and not element.tail.strip(): + element.tail = None + if element.tail is not None: + element.tail = element.tail.strip().replace("\n","").replace("\r","") + + return etree.tostring(root, encoding="utf-8", xml_declaration=True, pretty_print=pretty_print) diff --git a/s3tests/functional/test_s3_website.py b/s3tests/functional/test_s3_website.py index ca6444d..85a4e2b 100644 --- a/s3tests/functional/test_s3_website.py +++ b/s3tests/functional/test_s3_website.py @@ -4,6 +4,7 @@ import collections import nose import string import random +from pprint import pprint from urlparse import urlparse @@ -20,12 +21,15 @@ from . import ( ) from ..common import with_setup_kwargs +from ..xmlhelper import normalize_xml_whitespace + +IGNORE_FIELD = 'IGNORETHIS' WEBSITE_CONFIGS_XMLFRAG = { - 'IndexDoc': '${IndexDocument_Suffix}', - 'IndexDocErrorDoc': '${IndexDocument_Suffix}${ErrorDocument_Key}', - 'RedirectAll': '${RedirectAllRequestsTo_HostName}', - 'RedirectAll+Protocol': '${RedirectAllRequestsTo_HostName}${RedirectAllRequestsTo_Protocol}', + 'IndexDoc': '${IndexDocument_Suffix}${RoutingRules}', + 'IndexDocErrorDoc': '${IndexDocument_Suffix}${ErrorDocument_Key}${RoutingRules}', + 'RedirectAll': '${RedirectAllRequestsTo_HostName}${RoutingRules}', + 'RedirectAll+Protocol': '${RedirectAllRequestsTo_HostName}${RedirectAllRequestsTo_Protocol}${RoutingRules}', } def make_website_config(xml_fragment): @@ -34,23 +38,40 @@ def make_website_config(xml_fragment): """ return '' + xml_fragment + '' -def get_website_url(proto, bucket, path): +def get_website_url(**kwargs): """ Return the URL to a website page """ + proto, bucket, hostname, path = 'http', None, None, '/' + + if 'proto' in kwargs: + proto = kwargs['proto'] + if 'bucket' in kwargs: + bucket = kwargs['bucket'] + if 'hostname' in kwargs: + hostname = kwargs['hostname'] + if 'path' in kwargs: + path = kwargs['path'] + domain = config['main']['host'] if('s3website_domain' in config['main']): domain = config['main']['s3website_domain'] elif('s3website_domain' in config['alt']): domain = config['DEFAULT']['s3website_domain'] + if hostname is None: + hostname = '%s.%s' % (bucket, domain) path = path.lstrip('/') - return "%s://%s.%s/%s" % (proto, bucket, domain, path) + return "%s://%s/%s" % (proto, hostname, path) def _test_website_populate_fragment(xml_fragment, fields): + for k in ['RoutingRules']: + if k in fields.keys() and len(fields[k]) > 0: + fields[k] = '<%s>%s' % (k, fields[k], k) f = { 'IndexDocument_Suffix': choose_bucket_prefix(template='index-{random}.html', max_len=32), 'ErrorDocument_Key': choose_bucket_prefix(template='error-{random}.html', max_len=32), 'RedirectAllRequestsTo_HostName': choose_bucket_prefix(template='{random}.{random}.com', max_len=32), + 'RoutingRules': '' } f.update(fields) xml_fragment = string.Template(xml_fragment).safe_substitute(**f) @@ -58,10 +79,15 @@ def _test_website_populate_fragment(xml_fragment, fields): def _test_website_prep(bucket, xml_template, hardcoded_fields = {}): xml_fragment, f = _test_website_populate_fragment(xml_template, hardcoded_fields) - config_xml = make_website_config(xml_fragment) - print(config_xml) - bucket.set_website_configuration_xml(config_xml) - eq (config_xml, bucket.get_website_configuration_xml()) + config_xml1 = make_website_config(xml_fragment) + bucket.set_website_configuration_xml(config_xml1) + config_xml1 = normalize_xml_whitespace(config_xml1, pretty_print=True) # Do it late, so the system gets weird whitespace + #print("config_xml1\n", config_xml1) + config_xml2 = bucket.get_website_configuration_xml() + config_xml2 = normalize_xml_whitespace(config_xml2, pretty_print=True) # For us to read + #print("config_xml2\n", config_xml2) + eq (config_xml1, config_xml2) + f['WebsiteConfiguration'] = config_xml2 return f def __website_expected_reponse_status(res, status, reason): @@ -70,15 +96,19 @@ def __website_expected_reponse_status(res, status, reason): if not isinstance(reason, collections.Container): reason = set([reason]) - ok(res.status in status, 'HTTP status code mismatch') - ok(res.reason in reason, 'HTTP reason mismatch') + if status is not IGNORE_FIELD: + ok(res.status in status, 'HTTP code was %s should be %s' % (res.status, status)) + if reason is not IGNORE_FIELD: + ok(res.reason in reason, 'HTTP reason was was %s should be %s' % (res.reason, reason)) def _website_expected_error_response(res, bucket_name, status, reason, code): body = res.read() print(body) __website_expected_reponse_status(res, status, reason) - ok('
  • Code: '+code+'
  • ' in body, 'HTML should contain "Code: %s" ' % (code, )) - ok(('
  • BucketName: %s
  • ' % (bucket_name, )) in body, 'HTML should contain bucket name') + if code is not IGNORE_FIELD: + ok('
  • Code: '+code+'
  • ' in body, 'HTML should contain "Code: %s" ' % (code, )) + if bucket_name is not IGNORE_FIELD: + ok(('
  • BucketName: %s
  • ' % (bucket_name, )) in body, 'HTML should contain bucket name') def _website_expected_redirect_response(res, status, reason, new_url): body = res.read() @@ -89,7 +119,7 @@ def _website_expected_redirect_response(res, status, reason, new_url): ok(len(body) == 0, 'Body of a redirect should be empty') def _website_request(bucket_name, path, method='GET'): - url = get_website_url('http', bucket_name, path) + url = get_website_url(proto='http', bucket=bucket_name, path=path) print("url", url) o = urlparse(url) @@ -179,8 +209,8 @@ def test_website_private_bucket_list_public_index(): @attr('s3website') def test_website_private_bucket_list_empty(): bucket = get_new_bucket() - bucket.set_canned_acl('private') f = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDoc']) + bucket.set_canned_acl('private') res = _website_request(bucket.name, '') _website_expected_error_response(res, bucket.name, 403, 'Forbidden', 'AccessDenied') @@ -517,8 +547,7 @@ def test_website_private_bucket_list_private_index_gooderrordoc(): errorhtml.delete() bucket.delete() -# ------ redirect tests - +# ------ RedirectAll tests @attr(resource='bucket') @attr(method='get') @attr(operation='list') @@ -570,10 +599,212 @@ def test_website_bucket_private_redirectall_path_upgrade(): pathfragment = choose_bucket_prefix(template='/{random}', max_len=16) - res = _website_request(bucket.name, +pathfragment) + res = _website_request(bucket.name, pathfragment) # RGW returns "302 Found" per RFC2616 # S3 returns 302 Moved Temporarily per RFC1945 new_url = 'https://%s%s' % (f['RedirectAllRequestsTo_HostName'], pathfragment) _website_expected_redirect_response(res, 302, ['Found', 'Moved Temporarily'], new_url) bucket.delete() + +# RoutingRules +ROUTING_RULES = { + 'empty': '', + 'AmazonExample1': \ +""" + + + docs/ + + + documents/ + + +""", + 'AmazonExample1+Protocol=https': \ +""" + + + docs/ + + + https + documents/ + + +""", + 'AmazonExample1+Protocol=https+Hostname=xyzzy': \ +""" + + + docs/ + + + https + xyzzy + documents/ + + +""", + 'AmazonExample1+Protocol=http2': \ +""" + + + docs/ + + + http2 + documents/ + + +""", + 'AmazonExample2': \ +""" + + + images/ + + + folderdeleted.html + + +""", + 'AmazonExample2+HttpRedirectCode=314': \ +""" + + + images/ + + + 314 + folderdeleted.html + + +""", + 'AmazonExample3': \ +""" + + + 404 + + + ec2-11-22-333-44.compute-1.amazonaws.com + report-404/ + + +""", + 'AmazonExample3+KeyPrefixEquals': \ +""" + + + images/ + 404 + + + ec2-11-22-333-44.compute-1.amazonaws.com + report-404/ + + +""", +} + +ROUTING_RULES_TESTS = [ + dict(xml=dict(RoutingRules=ROUTING_RULES['empty']), url='', location=None, code=200), + dict(xml=dict(RoutingRules=ROUTING_RULES['empty']), url='/', location=None, code=200), + dict(xml=dict(RoutingRules=ROUTING_RULES['empty']), url='/x', location=None, code=404), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1']), url='/', location=None, code=200), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1']), url='/x', location=None, code=404), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1']), url='/docs/', location=dict(proto='http',bucket='{bucket_name}',path='/documents/'), code=301), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1']), url='/docs/x', location=dict(proto='http',bucket='{bucket_name}',path='/documents/x'), code=301), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1+Protocol=https']), url='/', location=None, code=200), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1+Protocol=https']), url='/x', location=None, code=404), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1+Protocol=https']), url='/docs/', location=dict(proto='https',bucket='{bucket_name}',path='/documents/'), code=301), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1+Protocol=https']), url='/docs/x', location=dict(proto='https',bucket='{bucket_name}',path='/documents/x'), code=301), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1+Protocol=http2']), url='/', location=None, code=200), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1+Protocol=http2']), url='/x', location=None, code=404), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1+Protocol=http2']), url='/docs/', location=dict(proto='http2',bucket='{bucket_name}',path='/documents/'), code=301), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1+Protocol=http2']), url='/docs/x', location=dict(proto='http2',bucket='{bucket_name}',path='/documents/x'), code=301), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1+Protocol=https+Hostname=xyzzy']), url='/', location=None, code=200), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1+Protocol=https+Hostname=xyzzy']), url='/x', location=None, code=404), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1+Protocol=https+Hostname=xyzzy']), url='/docs/', location=dict(proto='https',hostname='xyzzy',path='/documents/'), code=301), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample1+Protocol=https+Hostname=xyzzy']), url='/docs/x', location=dict(proto='https',hostname='xyzzy',path='/documents/x'), code=301), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample2']), url='/images/', location=dict(proto='http',bucket='{bucket_name}',path='/folderdeleted.html'), code=301), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample2']), url='/images/x', location=dict(proto='http',bucket='{bucket_name}',path='/folderdeleted.html'), code=301), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample2+HttpRedirectCode=314']), url='/images/', location=dict(proto='http',bucket='{bucket_name}',path='/folderdeleted.html'), code=314), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample2+HttpRedirectCode=314']), url='/images/x', location=dict(proto='http',bucket='{bucket_name}',path='/folderdeleted.html'), code=314), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample3']), url='/x', location=dict(proto='http',bucket='ec2-11-22-333-44.compute-1.amazonaws.com',path='/report-404/x'), code=301), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample3']), url='/images/x', location=dict(proto='http',bucket='ec2-11-22-333-44.compute-1.amazonaws.com',path='/report-404/images/x'), code=301), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample3+KeyPrefixEquals']), url='/x', location=None, code=404), + dict(xml=dict(RoutingRules=ROUTING_RULES['AmazonExample3+KeyPrefixEquals']), url='/images/x', location=dict(proto='http',bucket='ec2-11-22-333-44.compute-1.amazonaws.com',path='/report-404/x'), code=301), +] + +def routing_setup(): + kwargs = {'obj':[]} + bucket = get_new_bucket() + kwargs['bucket'] = bucket + kwargs['obj'].append(bucket) + f = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDocErrorDoc']) + kwargs.update(f) + bucket.set_canned_acl('public-read') + + k = bucket.new_key(f['IndexDocument_Suffix']) + kwargs['obj'].append(k) + s = choose_bucket_prefix(template='

    Index

    {random}', max_len=64) + k.set_contents_from_string(s) + k.set_canned_acl('public-read') + + k = bucket.new_key(f['ErrorDocument_Key']) + kwargs['obj'].append(k) + s = choose_bucket_prefix(template='

    Error

    {random}', max_len=64) + k.set_contents_from_string(s) + k.set_canned_acl('public-read') + + return kwargs + +def routing_teardown(**kwargs): + for o in reversed(kwargs['obj']): + print('Deleting', str(o)) + o.delete() + + +@with_setup_kwargs(setup=routing_setup, teardown=routing_teardown) +def routing_check(*args, **kwargs): + bucket = kwargs['bucket'] + args=args[0] + #print(args) + pprint(args) + xml_fields = kwargs.copy() + xml_fields.update(args['xml']) + pprint(xml_fields) + f = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDocErrorDoc'], hardcoded_fields=xml_fields) + #print(f) + config_xml2 = bucket.get_website_configuration_xml() + config_xml2 = normalize_xml_whitespace(config_xml2, pretty_print=True) # For us to read + res = _website_request(bucket.name, args['url']) + print(config_xml2) + # RGW returns "302 Found" per RFC2616 + # S3 returns 302 Moved Temporarily per RFC1945 + new_url = args['location'] + if new_url is not None: + new_url = get_website_url(**new_url) + new_url = new_url.format(bucket_name=bucket.name) + if args['code'] >= 200 and args['code'] < 300: + #body = res.read() + #print(body) + #eq(body, args['content'], 'default content should match index.html set content') + ok(res.getheader('Content-Length', -1) > 0) + elif args['code'] >= 300 and args['code'] < 400: + _website_expected_redirect_response(res, args['code'], IGNORE_FIELD, new_url) + elif args['code'] >= 400: + _website_expected_error_response(res, bucket.name, args['code'], IGNORE_FIELD, IGNORE_FIELD) + else: + assert(False) + +@attr('xml') +def testGEN_routing(): + + for t in ROUTING_RULES_TESTS: + yield routing_check, t + + +