[z3-checkins] r37889 - in z3/deliverance/DeliveranceVHoster/trunk: dvhoster tests

ianb at codespeak.net ianb at codespeak.net
Sun Feb 4 04:30:19 CET 2007


Author: ianb
Date: Sun Feb  4 04:30:14 2007
New Revision: 37889

Modified:
   z3/deliverance/DeliveranceVHoster/trunk/dvhoster/dataprovider.py
   z3/deliverance/DeliveranceVHoster/trunk/dvhoster/debuginterp.py
   z3/deliverance/DeliveranceVHoster/trunk/dvhoster/dispatcher.py
   z3/deliverance/DeliveranceVHoster/trunk/dvhoster/wsgiapp.py
   z3/deliverance/DeliveranceVHoster/trunk/tests/test_functional_api.py
Log:
Make the functional tests pass, w00t

Modified: z3/deliverance/DeliveranceVHoster/trunk/dvhoster/dataprovider.py
==============================================================================
--- z3/deliverance/DeliveranceVHoster/trunk/dvhoster/dataprovider.py	(original)
+++ z3/deliverance/DeliveranceVHoster/trunk/dvhoster/dataprovider.py	Sun Feb  4 04:30:14 2007
@@ -51,20 +51,93 @@
             print 'Creating data directory %s' % self.dir
             os.makedirs(self.dir)
 
-    def domain(self, domain_name):
+    def alias_fn(self, alias):
+        return os.path.join(self.dir, alias+'-alias.txt')        
+
+    def domain(self, domain_name, aliases=()):
+        """
+        Return the domain object for the given domain_name.  The
+        domain name given may not be the canonical domain.
+        """
         domain_name = self.normalize(domain_name)
+        alias_fn = self.alias_fn(domain_name)
+        if os.path.exists(alias_fn):
+            f = open(alias_fn, 'rb')
+            alias = f.read()
+            f.close()
+            aliases = list(aliases)
+            if alias in aliases:
+                raise ValueError(
+                    "Infinite alias loop (found: %s -> %s)"
+                    % (aliases, alias))
+            aliases.append(alias)
+            return self.domain(alias, aliases=aliases)
         dir = os.path.join(self.dir, domain_name)
         if not os.path.exists(dir):
             os.mkdir(dir)
         return DomainDataProvider(self, domain_name, dir)
 
     def normalize(self, domain_name):
+        """
+        Normalize a domain name (lower case, no funny characters).
+        """
         domain_name = domain_name.strip().lower()
         if not domain_re.search(domain_name):
             raise ValueError(
                 'Bad domain name: %r' % domain_name)
         return domain_name
 
+    def add_alias(self, alias, domain):
+        """
+        Add an alias domain ``alias`` which points to ``domain``.
+        There must not be an existing alias.
+        """
+        alias = self.normalize(alias)
+        domain = self.normalize(domain)
+        fn = self.alias_fn(alias)
+        assert not os.path.exists(fn), (
+            "Alias file already exists (%r)" % fn)
+        f = open(fn, 'wb')
+        f.write(domain)
+        f.close()
+
+    def remove_alias(self, alias, domain=None):
+        """
+        Remove the alias for the domain ``alias``.  ``domain`` (if
+        given) is what the alias should currently be pointing to (we
+        won't remove the alias then if it points elsewhere).
+        """
+        alias = self.normalize(alias)
+        if domain:
+            domain = self.normalize(domain)
+        fn = self.alias_fn(alias)
+        if not os.path.exists(fn):
+            raise ValueError(
+                "Alias does not exist (not file %r)" % fn)
+        if domain:
+            f = open(fn, 'rb')
+            existing = f.read().strip()
+            f.close()
+            if existing != domain:
+                raise ValueError(
+                    "Tried to remove alias from %s->%s, but %s actually "
+                    "points to %s" % (alias, domain, alias, existing))
+        os.unlink(fn)
+
+    def rename_domain(self, old_domain_name, new_domain_name):
+        """
+        Rename the old domain to the new domain, returning the new domain
+        object.
+        """
+        old_domain_name = self.normalize(old_domain_name)
+        new_domain_name = self.normalize(new_domain_name)
+        old_dir = os.path.join(self.dir, old_domain_name)
+        new_dir = os.path.join(self.dir, new_domain_name)
+        assert os.path.exists(old_dir)
+        assert not os.path.exists(new_dir)
+        os.rename(old_dir, new_dir)
+        return self.domain(new_domain_name)
+
 class DomainDataProvider(object):
 
     def __init__(self, provider, domain_name, base_dir):
@@ -95,7 +168,7 @@
 
     @property
     def initialized(self):
-        return hasattr(self, 'remote')
+        return hasattr(self, 'remote_uris')
 
     @property
     def rule_dir(self):
@@ -105,11 +178,29 @@
     def static_dir(self):
         return os.path.join(self.base_dir, 'static')
 
-    remote = persist.file_property('remote.txt')
+    remote_uris = descriptors.json_converter(
+        persist.file_property('remote_uris.txt'))
     theme_uri = persist.file_property('theme_uri.txt')
     theme_id = persist.file_property('theme_id.txt')
     rule_ids = persist.file_property('rule_ids.txt')
-    aliases = descriptors.line_converter(persist.file_property('aliases.txt'))
+    redirects = descriptors.json_converter(
+        persist.file_property('redirects.txt', default=()))
+
+    def aliases__rename(self, old_aliases, new_aliases):
+        dropped = list(old_aliases)
+        added = list(new_aliases)
+        for new_alias in new_aliases:
+            if new_alias in dropped:
+                dropped.remove(new_alias)
+                added.remove(new_alias)
+        for alias in dropped:
+            self.provider.remove_alias(alias, self.domain)
+        for alias in added:
+            self.provider.add_alias(alias, self.domain)
+    
+    aliases = descriptors.watcher(
+        descriptors.line_converter(persist.file_property('aliases.txt', default='')),
+        after_watcher=aliases__rename)
 
     def set_rule_file(self, filename, content):
         filename = os.path.join(self.rule_dir, filename)
@@ -119,11 +210,16 @@
 
     def domain__get(self):
         return self._domain_name
-
-    def domain__set(self, value):
-        new_obj = self.provider.rename_domain(self.domain, value)
+ 
+    def domain__set(self, new_domain):
+        aliases = self.aliases
+        old_domain = self.domain
+        new_obj = self.provider.rename_domain(self.domain, new_domain)
         # Clone new object as self:
         self.__dict__.update(new_obj.__dict__)
+        for alias in aliases:
+            self.provider.remove_alias(alias, domain=old_domain)
+            self.provider.add_alias(alias, new_domain)
 
     domain = property(domain__get, domain__set)
 
@@ -133,6 +229,7 @@
 class ListDictValidator(validators.FancyValidator):
     required_keys = ()
     optional_keys = ()
+    trail_slash_keys = ()
     
     def validate_python(self, value, state=None):
         # Should be like [{'path': path, 'remote_uri': uri, 'comment': str}]
@@ -158,14 +255,25 @@
         except AssertionError, e:
             raise validators.Invalid(str(e), value, state)
 
+    def _to_python(self, value, state=None):
+        for d in value:
+            for key in self.trail_slash_keys:
+                if key not in d:
+                    continue
+                if not d[key].endswith('/'):
+                    d[key] += '/'
+        return value
+
 class RemoteURIValidator(ListDictValidator):
     required_keys = ('path', 'remote_uri')
     optional_keys = ('comment', )
+    trail_slash_keys = ('path', 'remote_uri')
 
 class RewriteValidator(ListDictValidator):
     required_keys = ('rewrite', )
     # @@: Really we should require one of path or prefix
     optional_keys = ('path', 'prefix', 'comment')
+    trail_slash_keys = ('prefix', 'rewrite')
 
 class ProviderApp(server.ApplicationWrapper):
 
@@ -190,4 +298,4 @@
     def static(self):
         if not os.path.exists(self.static_dir):
             os.makedirs(self.static_dir)
-        return lildav.LilDAV(self.rule_dir)
+        return lildav.LilDAV(self.static_dir)

Modified: z3/deliverance/DeliveranceVHoster/trunk/dvhoster/debuginterp.py
==============================================================================
--- z3/deliverance/DeliveranceVHoster/trunk/dvhoster/debuginterp.py	(original)
+++ z3/deliverance/DeliveranceVHoster/trunk/dvhoster/debuginterp.py	Sun Feb  4 04:30:14 2007
@@ -17,7 +17,17 @@
         if not current_environ.get('dvhoster.has_errors'):
             current_environ['dvhoster.has_errors'] = True
             domain_info = current_environ['dvhoster.domain_info']
-            remote = domain_info.remote
+            remote_uris = domain_info.remote_uris
+            path_info = current_environ['PATH_INFO']
+            for remote_info in remote_uris:
+                if (path_info.startswith(remote_info['path'])
+                    or not path_info and remote_info['path'] == '/'):
+                    remote = remote_info['remote_uri']
+                    break
+            else:
+                assert 0, (
+                    "Nothing in remote_uris (%r) matches PATH_INFO=%r"
+                    % (remote_uris, path_info))
             remote += current_environ.get('PATH_INFO', '')
             if current_environ.get('QUERY_STRING'):
                 remote += '?' + current_environ['QUERY_STRING']

Modified: z3/deliverance/DeliveranceVHoster/trunk/dvhoster/dispatcher.py
==============================================================================
--- z3/deliverance/DeliveranceVHoster/trunk/dvhoster/dispatcher.py	(original)
+++ z3/deliverance/DeliveranceVHoster/trunk/dvhoster/dispatcher.py	Sun Feb  4 04:30:14 2007
@@ -1,5 +1,6 @@
 import posixpath
 import os
+import urlparse
 from paste.request import path_info_pop, construct_url
 from paste import httpexceptions
 from paste.urlparser import StaticURLParser
@@ -32,6 +33,7 @@
         domain = environ['HTTP_HOST']
         if ':' in domain:
             domain = domain.split(':', 1)[0]
+        domain = self.provider.normalize(domain)
         domain_info = self.provider.domain(domain)
         environ['dvhoster.domain_info'] = domain_info
         environ['dvhoster.base_url'] = construct_url(
@@ -42,12 +44,27 @@
             path_info_pop(environ)
             subapp = ProviderApp(domain_info)
             return subapp(environ, start_response)
+
+        if domain_info.domain != domain:
+            # We got an alias
+            assert domain in domain_info.aliases, (
+                "Domain %r not found in aliases %r"
+                % (domain, domain_info.aliases))
+            new_environ = environ.copy()
+            new_environ['HTTP_HOST'] = domain_info.domain
+            new_url = construct_url(new_environ)
+            exc = httpexceptions.HTTPMovedPermanently(
+                headers=[('Location', new_url)],
+                comment='Redirecting to canonical domain')
+            return exc(environ, start_response)
+        
         if path_info.startswith('/_rules'):
             path_info_pop(environ)
+            environ['QUERY_STRING'] = ''
             subapp = StaticURLParser(
                 domain_info.rule_dir)
             return subapp(environ, start_response)
-            
+        
         static_path = os.path.join(domain_info.static_dir,
                                    path_info.lstrip('/'))
         static_fn = self.find_file(static_path)
@@ -55,12 +72,69 @@
             # Explicit override of a file
             app = FileApp(static_fn)
             return app(environ, start_response)
+        try:
+            remote_uris = domain_info.remote_uris
+        except AttributeError:
+            exc = httpexceptions.HTTPNotFound(
+                "This domain has not yet been configured (no remote "
+                "URI has been configured for %s)"
+                % domain_info.domain)
+            return exc(environ, start_response)
+        remote_uri = None
+        current_uri = construct_url(environ)
+        for redirect_info in domain_info.redirects:
+            if 'path' in redirect_info:
+                if redirect_info['path'] == path_info:
+                    new = redirect_info['rewrite']
+                    new = urlparse.urljoin(current_uri, redirect_info['rewrite'])
+                    exc = httpexceptions.HTTPMovedPermanently(
+                        headers=[('Location', new)])
+                    return exc(environ, start_response)
+            elif 'prefix' in redirect_info:
+                assert redirect_info['prefix'].endswith('/')
+                new_uri = None
+                if path_info == redirect_info['prefix'][:-1]:
+                    new_uri = redirect_info['rewrite'] + '/'
+                elif path_info.startswith(redirect_info['prefix']):
+                    new_uri = redirect_info['rewrite']
+                    if not new_uri.endswith('/'):
+                        new_uri += '/'
+                    new_uri += path_info[len(redirect_info['prefix']):]
+                if new_uri is not None:
+                    new_uri = urlparse.urljoin(current_uri, new_uri)
+                    exc = httpexceptions.HTTPMovedPermanently(
+                        headers=[('Location', new_uri)])
+                    return exc(environ, start_response)
+                    
+        for remote_uri_info in remote_uris:
+            path = remote_uri_info['path']
+            if not path.endswith('/'):
+                path += '/'
+            if path_info + '/' == path:
+                exc = httpexceptions.HTTPMovedPermanently(
+                    headers=[('location', construct_url(environ, path_info=path_info+'/'))])
+                return exc(environ, start_response)
+            if path_info.startswith(path):
+                # Found a match
+                remote_uri = remote_uri_info['remote_uri']
+                environ['SCRIPT_NAME'] += path[:-1]
+                environ['PATH_INFO'] = path_info[len(path)-1:]
+                break
+        if not remote_uri:
+            exc = httpexceptions.HTTPNotFound(
+                "No URL is mapped to %r (somewhat oddly); only %s prefixes "
+                "are available"
+                % (path_info, ', '.join([repr(r['path'])
+                                         for r in remote_uris])))
+            return exc(environ, start_response)
+        print 'Proxy to %r with %r (SCRIPT_NAME=%r)' % (
+            remote_uri, construct_url(environ), environ['SCRIPT_NAME'])
         app = proxyapp.ForcedProxy(
-            remote=domain_info.remote,
+            remote=remote_uri,
             force_host=True)
         if self.rewrite_links:
             app = relocateresponse.RelocateMiddleware(
-                app, old_href=domain_info.remote)
+                app, old_href=remote_uri)
         rule_uri = construct_url(
             environ, with_query_string=False,
             path_info='/_rules/rule.xml')

Modified: z3/deliverance/DeliveranceVHoster/trunk/dvhoster/wsgiapp.py
==============================================================================
--- z3/deliverance/DeliveranceVHoster/trunk/dvhoster/wsgiapp.py	(original)
+++ z3/deliverance/DeliveranceVHoster/trunk/dvhoster/wsgiapp.py	Sun Feb  4 04:30:14 2007
@@ -3,6 +3,7 @@
 #from paste.deploy.config import ConfigMiddleware
 from paste.deploy.converters import asbool
 from paste.recursive import RecursiveMiddleware
+from paste.registry import RegistryManager
 from wsgifilter import proxyapp
 from paste.exceptions.errormiddleware import ErrorMiddleware
 
@@ -12,6 +13,7 @@
     """Create a WSGI application and return it"""
     app = DeliveranceDispatcher(app_conf)
     app = RecursiveMiddleware(app)
+    app = RegistryManager(app)
     debug = app_conf['debug'] = asbool(app_conf.get('debug', global_conf.get('debug')))
     if asbool(app_conf.get('debug_headers')):
         app = proxyapp.DebugHeaders(app, show_body=asbool(app_conf.get('debug_bodies')))

Modified: z3/deliverance/DeliveranceVHoster/trunk/tests/test_functional_api.py
==============================================================================
--- z3/deliverance/DeliveranceVHoster/trunk/tests/test_functional_api.py	(original)
+++ z3/deliverance/DeliveranceVHoster/trunk/tests/test_functional_api.py	Sun Feb  4 04:30:14 2007
@@ -4,15 +4,21 @@
 from paste.urlparser import StaticURLParser
 from dvhoster.wsgiapp import make_app
 import wsgi_intercept
+import httplib
+for attr in ['get_app', 'connect']:
+    setattr(httplib.HTTPConnection, attr,
+            getattr(wsgi_intercept.WSGI_HTTPConnection, attr).im_func)
 
 data_filename = os.path.join(os.path.dirname(__file__), 'test-data')
 wsgi_app = make_app({}, data_dir=data_filename)
 app = TestApp(wsgi_app)
 
 def put(uri, data):
-    app.post(uri, data, extra_environ={'REQUEST_METHOD': 'PUT'}, status=(201, 204))
+    return app.post(
+        uri, data, extra_environ={'REQUEST_METHOD': 'PUT'},
+        status=(201, 204))
 
-rule_data = '''
+rule_data = '''\
 <?xml version="1.0" encoding="UTF-8"?>
   <rules xmlns="http://www.plone.org/deliverance">
     <append-or-replace theme="//head" content="//head/title" />
@@ -53,7 +59,9 @@
     assert res.body == uri
 
     # Rules:
-    put('/.deliverance/rules/rules.xml', rule_data)
+    put('/.deliverance/rules/rule.xml', rule_data)
+    assert app.get('/.deliverance/rules/rule.xml').body == rule_data
+    assert app.get('/_rules/rule.xml').body == rule_data
 
     # Domain (no rename test):
     res = app.get('/.deliverance/domain')
@@ -65,11 +73,12 @@
     assert app.get('/.deliverance/aliases').body == data
 
     data = '''
-    [{"path": "/", "remote_uri": "http://wsgify.org/"},
-     {"path": "/bar", "remote_uri": "http://wsgify.org/blah", "comment": "x"}]
+    [{"path": "/bar", "remote_uri": "http://wsgify.org/blah", "comment": "x"},
+     {"path": "/", "remote_uri": "http://wsgify.org/"}]
      '''
 
     put('/.deliverance/remote_uris', data)
+    # It gets normalized, so it doesn't actually stay the same:
     #assert app.get('/.deliverance/remote_uris').body == data
 
     data = '''
@@ -85,7 +94,7 @@
     assert res.body == data
     app.get('/.deliverance/static/subdir',
             extra_environ={'REQUEST_METHOD': 'MKCOL'}, status=201)
-    put('/.deliverance/static/subdir/foo.html', 'blah')
+    res = put('/.deliverance/static/subdir/foo.html', 'blah')
     res = app.get('/.deliverance/static/subdir/foo.html')
     assert res.body == 'blah'
     res = app.get('/subdir/foo.html')
@@ -106,14 +115,14 @@
     assert 'unthemed' not in res
     # Redirect non-canonical domains:
     res = app.get('/index.html', extra_environ=dict(HTTP_HOST='example.com'),
-                  status=303)
+                  status=301)
     assert res.header('location') == 'http://%s/index.html' % hostname
     # Test the path backend redirect:
     res = app.get('/bar/foo.html')
     res.mustcontain('foo')
-    res = app.get('/test1.html', status=303)
-    assert res.header('location') == 'http://%s/test1' % hostname
-    res = app.get('/test2/other/stuff.html', status=303)
+    res = app.get('/test1.html', status=301)
+    assert res.header('location') == 'http://%s/test1/' % hostname
+    res = app.get('/test2/other/stuff.html', status=301)
     assert res.header('location') == 'http://%s/test3/other/stuff.html' % hostname
     res = app.get('/test1.html/foo/bar', status=404)
     res = app.get('/data.html')
@@ -128,4 +137,4 @@
     res = app.get('/.deliverance/aliases')
     res.mustcontain('localhost')
     assert app.get('/.deliverance/domain').body == 'localhost2'
-    
+    print 'site renamed to localhost2'


More information about the z3-checkins mailing list