diff options
| -rw-r--r-- | CHANGES.txt | 10 | ||||
| -rw-r--r-- | CONTRIBUTORS.txt | 3 | ||||
| -rw-r--r-- | docs/tutorials/wiki/authorization.rst | 218 | ||||
| -rw-r--r-- | pyramid/authentication.py | 24 | ||||
| -rw-r--r-- | pyramid/paster.py | 31 | ||||
| -rw-r--r-- | pyramid/static.py | 61 | ||||
| -rw-r--r-- | pyramid/tests/test_integration.py | 59 | ||||
| -rw-r--r-- | pyramid/tests/test_paster.py | 75 | ||||
| -rw-r--r-- | pyramid/tests/test_static.py | 95 |
9 files changed, 413 insertions, 163 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 0bd19572a..3ae834d93 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -104,6 +104,16 @@ Bug Fixes DummyRequest instead of eagerly assigning an attribute. See also https://github.com/Pylons/pyramid/issues/165 +- When visiting a URL that represented a static view which resolved to a + subdirectory, the ``index.html`` of that subdirectory would not be served + properly. Instead, a redirect to ``/subdir`` would be issued. This has + been fixed, and now visiting a subdirectory that contains an ``index.html`` + within a static view returns the index.html properly. See also + https://github.com/Pylons/pyramid/issues/67. + +- Redirects issued by a static view did not take into account any existing + ``SCRIPT_NAME`` (such as one set by a url mapping composite). Now they do. + 1.0 (2011-01-30) ================ diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 97cae7c3e..5a72f242e 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -132,3 +132,6 @@ Contributors - Malthe Borch, 2011/02/28 - Joel Bohman, 2011/04/16 + +- Juliusz Gonera, 2011/04/17 + diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index ee86eb543..e4480d6d9 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -7,21 +7,25 @@ edit, and add pages to our wiki. For purposes of demonstration we'll change our application to allow people whom are members of a *group* named ``group:editors`` to add and edit wiki pages but we'll continue allowing anyone with access to the server to view pages. :app:`Pyramid` provides -facilities for *authorization* and *authentication*. We'll make use of both -features to provide security to our application. +facilities for :term:`authorization` and :term:`authentication`. We'll make +use of both features to provide security to our application. -The source code for this tutorial stage can be browsed via -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/ -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/>`_. +We will add an :term:`authentication policy` and an +:term:`authorization policy` to our :term:`application +registry`, add a ``security.py`` module and give our :term:`root` +resource an :term:`ACL`. +Then we will add ``login`` and ``logout`` views, and modify the +existing views to make them return a ``logged_in`` flag to the +renderer and add :term:`permission` declarations to their ``view_config`` +decorators. -Configuring a ``pyramid`` Authentication Policy --------------------------------------------------- +Finally, we will add a ``login.pt`` template and change the existing +``view.pt`` and ``edit.pt`` to show a "Logout" link when not logged in. -For any :app:`Pyramid` application to perform authorization, we need to add a -``security.py`` module and we'll need to change our :term:`application -registry` to add an :term:`authentication policy` and a :term:`authorization -policy`. +The source code for this tutorial stage can be browsed via +`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/ +<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/>`_. Adding Authentication and Authorization Policies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -64,6 +68,43 @@ user and groups sources. Note that the ``editor`` user is a member of the ``group:editors`` group in our dummy group data (the ``GROUPS`` data structure). +Giving Our Root Resource an ACL +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We need to give our root resource object an :term:`ACL`. This ACL will be +sufficient to provide enough information to the :app:`Pyramid` security +machinery to challenge a user who doesn't have appropriate credentials when +he attempts to invoke the ``add_page`` or ``edit_page`` views. + +We need to perform some imports at module scope in our ``models.py`` file: + +.. code-block:: python + :linenos: + + from pyramid.security import Allow + from pyramid.security import Everyone + +Our root resource object is a ``Wiki`` instance. We'll add the following +line at class scope to our ``Wiki`` class: + +.. code-block:: python + :linenos: + + __acl__ = [ (Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit') ] + +It's only happenstance that we're assigning this ACL at class scope. An ACL +can be attached to an object *instance* too; this is how "row level security" +can be achieved in :app:`Pyramid` applications. We actually only need *one* +ACL for the entire system, however, because our security requirements are +simple, so this feature is not demonstrated. + +Our resulting ``models.py`` file will now look like so: + +.. literalinclude:: src/authorization/tutorial/models.py + :linenos: + :language: python + Adding Login and Logout Views ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -129,6 +170,38 @@ template. For example: logged_in = logged_in, edit_url = edit_url) +Adding ``permission`` Declarations to our ``view_config`` Decorators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To protect each of our views with a particular permission, we need to pass a +``permission`` argument to each of our :class:`pyramid.view.view_config` +decorators. To do so, within ``views.py``: + +- We add ``permission='view'`` to the decorator attached to the + ``view_wiki`` and ``view_page`` view functions. This makes the + assertion that only users who possess the ``view`` permission + against the context resource at the time of the request may + invoke these views. We've granted + :data:`pyramid.security.Everyone` the view permission at the + root model via its ACL, so everyone will be able to invoke the + ``view_wiki`` and ``view_page`` views. + +- We add ``permission='edit'`` to the decorator attached to the + ``add_page`` and ``edit_page`` view functions. This makes the + assertion that only users who possess the effective ``edit`` + permission against the context resource at the time of the + request may invoke these views. We've granted the + ``group:editors`` principal the ``edit`` permission at the + root model via its ACL, so only a user whom is a member of + the group named ``group:editors`` will able to invoke the + ``add_page`` or ``edit_page`` views. We've likewise given + the ``editor`` user membership to this group via the + ``security.py`` file by mapping him to the ``group:editors`` + group in the ``GROUPS`` data structure (``GROUPS + = {'editor':['group:editors']}``); the ``groupfinder`` + function consults the ``GROUPS`` data structure. This means + that the ``editor`` user can add and edit pages. + Adding the ``login.pt`` Template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -154,92 +227,29 @@ class="app-welcome align-right">`` div: <a href="${request.application_url}/logout">Logout</a> </span> -Giving Our Root Resource an ACL -------------------------------- - -We need to give our root resource object an :term:`ACL`. This ACL will be -sufficient to provide enough information to the :app:`Pyramid` security -machinery to challenge a user who doesn't have appropriate credentials when -he attempts to invoke the ``add_page`` or ``edit_page`` views. +Seeing Our Changes To ``views.py`` and our Templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -We need to perform some imports at module scope in our ``models.py`` file: +Our ``views.py`` module will look something like this when we're done: -.. code-block:: python +.. literalinclude:: src/authorization/tutorial/views.py :linenos: + :language: python - from pyramid.security import Allow - from pyramid.security import Everyone - -Our root resource object is a ``Wiki`` instance. We'll add the following -line at class scope to our ``Wiki`` class: +Our ``edit.pt`` template will look something like this when we're done: -.. code-block:: python +.. literalinclude:: src/authorization/tutorial/templates/edit.pt :linenos: + :language: xml - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] - -It's only happenstance that we're assigning this ACL at class scope. An ACL -can be attached to an object *instance* too; this is how "row level security" -can be achieved in :app:`Pyramid` applications. We actually only need *one* -ACL for the entire system, however, because our security requirements are -simple, so this feature is not demonstrated. - -Our resulting ``models.py`` file will now look like so: +Our ``view.pt`` template will look something like this when we're done: -.. literalinclude:: src/authorization/tutorial/models.py +.. literalinclude:: src/authorization/tutorial/templates/view.pt :linenos: - :language: python - -Adding ``permission`` Declarations to our ``view_config`` Decorators --------------------------------------------------------------------- - -To protect each of our views with a particular permission, we need to pass a -``permission`` argument to each of our :class:`pyramid.view.view_config` -decorators. To do so, within ``views.py``: - -- We add ``permission='view'`` to the decorator attached to the ``view_wiki`` - view function. This makes the assertion that only users who possess the - ``view`` permission against the context resource at the time of the request - may invoke this view. We've granted :data:`pyramid.security.Everyone` the - view permission at the root model via its ACL, so everyone will be able to - invoke the ``view_wiki`` view. - -- We add ``permission='view'`` to the decorator attached to the ``view_page`` - view function. This makes the assertion that only users who possess the - effective ``view`` permission against the context resource at the time of - the request may invoke this view. We've granted - :data:`pyramid.security.Everyone` the view permission at the root model via - its ACL, so everyone will be able to invoke the ``view_page`` view. - -- We add ``permission='edit'`` to the decorator attached to the ``add_page`` - view function. This makes the assertion that only users who possess the - effective ``edit`` permission against the context resource at the time of - the request may invoke this view. We've granted the ``group:editors`` - principal the ``edit`` permission at the root model via its ACL, so only - the a user whom is a member of the group named ``group:editors`` will able - to invoke the ``add_page`` view. We've likewise given the ``editor`` user - membership to this group via thes ``security.py`` file by mapping him to - the ``group:editors`` group in the ``GROUPS`` data structure (``GROUPS = - {'editor':['group:editors']}``); the ``groupfinder`` function consults the - ``GROUPS`` data structure. This means that the ``editor`` user can add - pages. - -- We add ``permission='edit'`` to the decorator attached to the ``edit_page`` - view function. This makes the assertion that only users who possess the - effective ``edit`` permission against the context resource at the time of - the request may invoke this view. We've granted the ``group:editors`` - principal the ``edit`` permission at the root model via its ACL, so only - the a user whom is a member of the group named ``group:editors`` will able - to invoke the ``edit_page`` view. We've likewise given the ``editor`` user - membership to this group via thes ``security.py`` file by mapping him to - the ``group:editors`` group in the ``GROUPS`` data structure (``GROUPS = - {'editor':['group:editors']}``); the ``groupfinder`` function consults the - ``GROUPS`` data structure. This means that the ``editor`` user can edit - pages. + :language: xml Viewing the Application in a Browser ------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can finally examine our application in a browser. The views we'll try are as follows: @@ -267,35 +277,7 @@ as follows: credentials with the username ``editor``, password ``editor`` will show the edit page form being displayed. -Seeing Our Changes To ``views.py`` and our Templates ----------------------------------------------------- - -Our ``views.py`` module will look something like this when we're done: - -.. literalinclude:: src/authorization/tutorial/views.py - :linenos: - :language: python - -Our ``edit.pt`` template will look something like this when we're done: - -.. literalinclude:: src/authorization/tutorial/templates/edit.pt - :linenos: - :language: xml - -Our ``view.pt`` template will look something like this when we're done: - -.. literalinclude:: src/authorization/tutorial/templates/view.pt - :linenos: - :language: xml - -Revisiting the Application ---------------------------- - -When we revisit the application in a browser, and log in (as a result -of hitting an edit or add page and submitting the login form with the -``editor`` credentials), we'll see a Logout link in the upper right -hand corner. When we click it, we're logged out, and redirected back -to the front page. - - - +- After logging in (as a result of hitting an edit or add page and + submitting the login form with the ``editor`` credentials), we'll see + a Logout link in the upper right hand corner. When we click it, + we're logged out, and redirected back to the front page. diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 78349854b..a6c74e549 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -61,12 +61,12 @@ class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy): ``callback`` - Default: ``None``. A callback passed the :mod:`repoze.who` - identity and the :term:`request`, expected to return ``None`` - if the user represented by the identity doesn't exist or a - sequence of group identifiers (possibly empty) if the user - does exist. If ``callback`` is None, the userid will be - assumed to exist with no groups. + Default: ``None``. A callback passed the :mod:`repoze.who` identity + and the :term:`request`, expected to return ``None`` if the user + represented by the identity doesn't exist or a sequence of principal + identifiers (possibly empty) representing groups if the user does + exist. If ``callback`` is None, the userid will be assumed to exist + with no group principals. Objects of this class implement the interface described by :class:`pyramid.interfaces.IAuthenticationPolicy`. @@ -149,10 +149,10 @@ class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy): ``callback`` Default: ``None``. A callback passed the userid and the request, - expected to return None if the userid doesn't exist or a sequence - of group identifiers (possibly empty) if the user does exist. - If ``callback`` is None, the userid will be assumed to exist with no - groups. + expected to return None if the userid doesn't exist or a sequence of + principal identifiers (possibly empty) representing groups if the + user does exist. If ``callback`` is None, the userid will be assumed + to exist with no group principals. Objects of this class implement the interface described by :class:`pyramid.interfaces.IAuthenticationPolicy`. @@ -187,9 +187,9 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): Default: ``None``. A callback passed the userid and the request, expected to return ``None`` if the userid doesn't - exist or a sequence of group identifiers (possibly empty) if + exist or a sequence of principal identifiers (possibly empty) if the user does exist. If ``callback`` is ``None``, the userid - will be assumed to exist with no groups. Optional. + will be assumed to exist with no principals. Optional. ``cookie_name`` diff --git a/pyramid/paster.py b/pyramid/paster.py index bc1573fb8..f9f8925d7 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -10,7 +10,7 @@ from paste.util.template import paste_script_template_renderer from pyramid.scripting import get_root class PyramidTemplate(Template): - def pre(self, command, output_dir, vars): # pragma: no cover + def pre(self, command, output_dir, vars): vars['random_string'] = os.urandom(20).encode('hex') package_logger = vars['package'] if package_logger == 'root': @@ -19,9 +19,12 @@ class PyramidTemplate(Template): vars['package_logger'] = package_logger return Template.pre(self, command, output_dir, vars) - def post(self, *arg, **kw): # pragma: no cover - print 'Welcome to Pyramid. Sorry for the convenience.' - return Template.post(self, *arg, **kw) + def post(self, command, output_dir, vars): + self.out('Welcome to Pyramid. Sorry for the convenience.') + return Template.post(self, command, output_dir, vars) + + def out(self, msg): # pragma: no cover (replaceable testing hook) + print msg class StarterProjectTemplate(PyramidTemplate): _template_dir = 'paster_templates/starter' @@ -88,7 +91,7 @@ class PShellCommand(PCommand): command will almost certainly fail. """ - summary = "Open an interactive shell with a pyramid app loaded" + summary = "Open an interactive shell with a Pyramid application loaded" min_args = 2 max_args = 2 @@ -100,10 +103,11 @@ class PShellCommand(PCommand): help="Don't use IPython even if it is available") def command(self, IPShell=_marker): - if IPShell is _marker: - try: #pragma no cover + # IPShell passed to command method is for testing purposes + if IPShell is _marker: # pragma: no cover + try: from IPython.Shell import IPShell - except ImportError: #pragma no cover + except ImportError: IPShell = None cprt =('Type "help" for more information. "root" is the Pyramid app ' 'root object, "registry" is the Pyramid registry object.') @@ -113,16 +117,17 @@ class PShellCommand(PCommand): app = self.get_app(config_file, section_name, loadapp=self.loadapp[0]) root, closer = self.get_root(app) shell_globals = {'root':root, 'registry':app.registry} - if IPShell is not None and not self.options.disable_ipython: + + if (IPShell is None) or self.options.disable_ipython: try: - shell = IPShell(argv=[], user_ns=shell_globals) - shell.IP.BANNER = shell.IP.BANNER + '\n\n' + banner - shell.mainloop() + self.interact[0](banner, local=shell_globals) finally: closer() else: try: - self.interact[0](banner, local=shell_globals) + shell = IPShell(argv=[], user_ns=shell_globals) + shell.IP.BANNER = shell.IP.BANNER + '\n\n' + banner + shell.mainloop() finally: closer() diff --git a/pyramid/static.py b/pyramid/static.py index 3866126ac..223652768 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -37,21 +37,18 @@ class PackageURLParser(StaticURLParser): filename = request.path_info_pop(environ) resource = os.path.normcase(os.path.normpath( self.resource_name + '/' + filename)) - if ( (self.root_resource is not None) and - (not resource.startswith(self.root_resource)) ): + if not resource.startswith(self.root_resource): # Out of bounds return self.not_found(environ, start_response) if not pkg_resources.resource_exists(self.package_name, resource): return self.not_found(environ, start_response) if pkg_resources.resource_isdir(self.package_name, resource): # @@: Cache? - child_root = (self.root_resource is not None and - self.root_resource or self.resource_name) return self.__class__( - self.package_name, resource, root_resource=child_root, + self.package_name, resource, root_resource=self.resource_name, cache_max_age=self.cache_max_age)(environ, start_response) - if (environ.get('PATH_INFO') - and environ.get('PATH_INFO') != '/'): # pragma: no cover + pi = environ.get('PATH_INFO') + if pi and pi != '/': return self.error_extra_path(environ, start_response) full = pkg_resources.resource_filename(self.package_name, resource) if_none_match = environ.get('HTTP_IF_NONE_MATCH') @@ -154,8 +151,8 @@ class StaticURLInfo(object): extra['view_permission'] = permission extra['view'] = view - # register a route using the computed view, permission, and pattern, - # plus any extras passed to us via add_static_view + # register a route using the computed view, permission, and + # pattern, plus any extras passed to us via add_static_view pattern = "%s*subpath" % name # name already ends with slash self.config.add_route(name, pattern, **extra) self.registrations.append((name, spec, False)) @@ -211,12 +208,44 @@ class static_view(object): self.app = app def __call__(self, context, request): - subpath = '/'.join(request.subpath) + # Point PATH_INFO to the static file/dir path; point SCRIPT_NAME + # to the prefix before it. + + # Postconditions: + # - SCRIPT_NAME and PATH_INFO are empty or start with / + # - At least one of SCRIPT_NAME or PATH_INFO are set. + # - SCRIPT_NAME is not '/' (it should be '', and PATH_INFO should + # be '/'). + request_copy = request.copy() - # Fix up PATH_INFO to get rid of everything but the "subpath" - # (the actual path to the file relative to the root dir). - request_copy.environ['PATH_INFO'] = '/' + subpath - # Zero out SCRIPT_NAME for good measure. - request_copy.environ['SCRIPT_NAME'] = '' - return request_copy.get_response(self.app) + script_name = request_copy.environ.get('SCRIPT_NAME', '') + path_info = request_copy.environ.get('PATH_INFO', '/') + + new_script_name = script_name + new_path_info = path_info + + subpath = list(request.subpath) + + if subpath: + # compute new_path_info + new_path_info = '/' + '/'.join(subpath) + if path_info.endswith('/'): + # readd trailing slash stripped by subpath (traversal) + # conversion + new_path_info += '/' + + # compute new_script_name + tmp = [] + workback = (script_name + path_info).split('/') + while workback: + el = workback.pop() + if el: + tmp.insert(0, el) + if tmp == subpath: + new_script_name = '/'.join(workback) + break + + request_copy.environ['SCRIPT_NAME'] = new_script_name + request_copy.environ['PATH_INFO'] = new_path_info + return request_copy.get_response(self.app) diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py index dc7525080..095f22f41 100644 --- a/pyramid/tests/test_integration.py +++ b/pyramid/tests/test_integration.py @@ -45,7 +45,7 @@ here = os.path.dirname(__file__) staticapp = static(os.path.join(here, 'fixtures')) class TestStaticApp(unittest.TestCase): - def test_it(self): + def test_basic(self): from webob import Request context = DummyContext() from StringIO import StringIO @@ -57,13 +57,68 @@ class TestStaticApp(unittest.TestCase): 'wsgi.version':(1,0), 'wsgi.url_scheme':'http', 'wsgi.input':StringIO()}) - request.subpath = ['minimal.pt'] + request.subpath = ('minimal.pt',) result = staticapp(context, request) self.assertEqual(result.status, '200 OK') self.assertEqual( result.body.replace('\r', ''), open(os.path.join(here, 'fixtures/minimal.pt'), 'r').read()) + def test_file_in_subdir(self): + from webob import Request + context = DummyContext() + from StringIO import StringIO + request = Request({'PATH_INFO':'', + 'SCRIPT_NAME':'', + 'SERVER_NAME':'localhost', + 'SERVER_PORT':'80', + 'REQUEST_METHOD':'GET', + 'wsgi.version':(1,0), + 'wsgi.url_scheme':'http', + 'wsgi.input':StringIO()}) + request.subpath = ('static', 'index.html',) + result = staticapp(context, request) + self.assertEqual(result.status, '200 OK') + self.assertEqual( + result.body.replace('\r', ''), + open(os.path.join(here, 'fixtures/static/index.html'), 'r').read()) + + def test_redirect_to_subdir(self): + from webob import Request + context = DummyContext() + from StringIO import StringIO + request = Request({'PATH_INFO':'', + 'SCRIPT_NAME':'', + 'SERVER_NAME':'localhost', + 'SERVER_PORT':'80', + 'REQUEST_METHOD':'GET', + 'wsgi.version':(1,0), + 'wsgi.url_scheme':'http', + 'wsgi.input':StringIO()}) + request.subpath = ('static',) + result = staticapp(context, request) + self.assertEqual(result.status, '301 Moved Permanently') + self.assertEqual(result.location, 'http://localhost/static/') + + def test_redirect_to_subdir_with_existing_script_name(self): + from webob import Request + context = DummyContext() + from StringIO import StringIO + request = Request({'PATH_INFO':'', + 'SCRIPT_NAME':'/script_name', + 'SERVER_NAME':'localhost', + 'SERVER_PORT':'80', + 'REQUEST_METHOD':'GET', + 'wsgi.version':(1,0), + 'wsgi.url_scheme':'http', + 'wsgi.input':StringIO()}) + request.subpath = ['static'] + result = staticapp(context, request) + self.assertEqual(result.status, '301 Moved Permanently') + self.assertEqual(result.location, + 'http://localhost/script_name/static/') + + class IntegrationBase(unittest.TestCase): root_factory = None package = None diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py index 35349b7c7..07ec4f7b7 100644 --- a/pyramid/tests/test_paster.py +++ b/pyramid/tests/test_paster.py @@ -1,5 +1,39 @@ import unittest +class TestPyramidTemplate(unittest.TestCase): + def _getTargetClass(self): + from pyramid.paster import PyramidTemplate + return PyramidTemplate + + def _makeOne(self, name): + cls = self._getTargetClass() + return cls(name) + + def test_pre_logger_eq_root(self): + tmpl = self._makeOne('name') + vars = {'package':'root'} + result = tmpl.pre(None, None, vars) + self.assertEqual(result, None) + self.assertEqual(vars['package_logger'], 'app') + self.failUnless(len(vars['random_string']) == 40) + + def test_pre_logger_noteq_root(self): + tmpl = self._makeOne('name') + vars = {'package':'notroot'} + result = tmpl.pre(None, None, vars) + self.assertEqual(result, None) + self.assertEqual(vars['package_logger'], 'notroot') + self.failUnless(len(vars['random_string']) == 40) + + def test_post(self): + tmpl = self._makeOne('name') + vars = {'package':'root'} + L = [] + tmpl.out = lambda msg: L.append(msg) + result = tmpl.post(None, None, vars) + self.assertEqual(result, None) + self.assertEqual(L, ['Welcome to Pyramid. Sorry for the convenience.']) + class TestPShellCommand(unittest.TestCase): def _getTargetClass(self): from pyramid.paster import PShellCommand @@ -8,7 +42,7 @@ class TestPShellCommand(unittest.TestCase): def _makeOne(self): return self._getTargetClass()('pshell') - def test_command_ipython_disabled(self): + def test_command_ipshell_is_None_ipython_enabled(self): command = self._makeOne() interact = DummyInteractor() app = DummyApp() @@ -18,7 +52,7 @@ class TestPShellCommand(unittest.TestCase): command.args = ('/foo/bar/myapp.ini', 'myapp') class Options(object): pass command.options = Options() - command.options.disable_ipython =True + command.options.disable_ipython = False command.command(IPShell=None) self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini') self.assertEqual(loadapp.section_name, 'myapp') @@ -32,6 +66,30 @@ class TestPShellCommand(unittest.TestCase): self.failUnless(interact.banner) self.assertEqual(len(app.threadlocal_manager.popped), 1) + def test_command_ipshell_is_not_None_ipython_disabled(self): + command = self._makeOne() + interact = DummyInteractor() + app = DummyApp() + loadapp = DummyLoadApp(app) + command.interact = (interact,) + command.loadapp = (loadapp,) + command.args = ('/foo/bar/myapp.ini', 'myapp') + class Options(object): pass + command.options = Options() + command.options.disable_ipython = True + command.command(IPShell='notnone') + self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini') + self.assertEqual(loadapp.section_name, 'myapp') + self.failUnless(loadapp.relative_to) + self.assertEqual(len(app.threadlocal_manager.pushed), 1) + pushed = app.threadlocal_manager.pushed[0] + self.assertEqual(pushed['registry'], dummy_registry) + self.assertEqual(pushed['request'].registry, dummy_registry) + self.assertEqual(interact.local, {'root':dummy_root, + 'registry':dummy_registry}) + self.failUnless(interact.banner) + self.assertEqual(len(app.threadlocal_manager.popped), 1) + def test_command_ipython_enabled(self): command = self._makeOne() app = DummyApp() @@ -133,6 +191,19 @@ class TestPRoutesCommand(unittest.TestCase): self.assertEqual(result, None) self.assertEqual(L, []) + def test_no_mapper(self): + command = self._makeOne() + command._get_mapper = lambda *arg:None + L = [] + command.out = L.append + app = DummyApp() + loadapp = DummyLoadApp(app) + command.loadapp = (loadapp,) + command.args = ('/foo/bar/myapp.ini', 'myapp') + result = command.command() + self.assertEqual(result, None) + self.assertEqual(L, []) + def test_single_route_no_route_registered(self): command = self._makeOne() route = DummyRoute('a', '/a') diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index acf5a754b..9c4c4a1c8 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -92,6 +92,14 @@ class TestPackageURLParser(unittest.TestCase): body = response[0] self.failUnless('<html>static</html>' in body) + def test_resource_has_extra_path_info(self): + environ = self._makeEnviron(PATH_INFO='/static/index.html/more') + inst = self._makeOne('pyramid.tests', 'fixtures') + sr = DummyStartResponse() + response = inst(environ, sr) + body = response[0] + self.failUnless("The trailing path '/more' is not allowed" in body) + def test_resource_is_file_with_cache_max_age(self): environ = self._makeEnviron(PATH_INFO='/index.html') inst = self._makeOne('pyramid.tests', 'fixtures/static', @@ -122,6 +130,15 @@ class TestPackageURLParser(unittest.TestCase): ['Accept-Ranges', 'Content-Length', 'Content-Range', 'Content-Type', 'ETag', 'Last-Modified']) + def test_with_root_resource(self): + environ = self._makeEnviron(PATH_INFO='/static/index.html') + inst = self._makeOne('pyramid.tests', 'fixtures', + root_resource='fixtures/static') + sr = DummyStartResponse() + response = inst(environ, sr) + body = response[0] + self.failUnless('<html>static</html>' in body) + def test_if_none_match(self): class DummyEq(object): def __eq__(self, other): @@ -136,6 +153,18 @@ class TestPackageURLParser(unittest.TestCase): self.assertEqual(sr.headerlist[0][0], 'ETag') self.assertEqual(response[0], '') + def test_if_none_match_miss(self): + class DummyEq(object): + def __eq__(self, other): + return False + dummy_eq = DummyEq() + environ = self._makeEnviron(HTTP_IF_NONE_MATCH=dummy_eq) + inst = self._makeOne('pyramid.tests', 'fixtures/static') + sr = DummyStartResponse() + inst(environ, sr) + self.assertEqual(len(sr.headerlist), 6) + self.assertEqual(sr.status, '200 OK') + def test_repr(self): import os.path inst = self._makeOne('pyramid.tests', 'fixtures/static') @@ -230,6 +259,53 @@ class Test_static_view(unittest.TestCase): self.assertEqual(response.package_name, 'another') self.assertEqual(response.cache_max_age, 3600) + def test_no_subpath_preserves_path_info_and_script_name(self): + view = self._makeOne('fixtures', package_name='another') + context = DummyContext() + request = DummyRequest() + request.subpath = () + request.environ = self._makeEnviron(PATH_INFO='/path_info', + SCRIPT_NAME='/script_name') + view(context, request) + self.assertEqual(request.copied, True) + self.assertEqual(request.environ['PATH_INFO'], '/path_info') + self.assertEqual(request.environ['SCRIPT_NAME'], '/script_name') + + def test_with_subpath_path_info_ends_with_slash(self): + view = self._makeOne('fixtures', package_name='another') + context = DummyContext() + request = DummyRequest() + request.subpath = ('subpath',) + request.environ = self._makeEnviron(PATH_INFO='/path_info/subpath/') + view(context, request) + self.assertEqual(request.copied, True) + self.assertEqual(request.environ['PATH_INFO'], '/subpath/') + self.assertEqual(request.environ['SCRIPT_NAME'], '/path_info') + + def test_with_subpath_original_script_name_preserved(self): + view = self._makeOne('fixtures', package_name='another') + context = DummyContext() + request = DummyRequest() + request.subpath = ('subpath',) + request.environ = self._makeEnviron(PATH_INFO='/path_info/subpath/', + SCRIPT_NAME='/scriptname') + view(context, request) + self.assertEqual(request.copied, True) + self.assertEqual(request.environ['PATH_INFO'], '/subpath/') + self.assertEqual(request.environ['SCRIPT_NAME'], + '/scriptname/path_info') + + def test_with_subpath_new_script_name_fixes_trailing_double_slashes(self): + view = self._makeOne('fixtures', package_name='another') + context = DummyContext() + request = DummyRequest() + request.subpath = ('sub', 'path') + request.environ = self._makeEnviron(PATH_INFO='/path_info//sub//path//') + view(context, request) + self.assertEqual(request.copied, True) + self.assertEqual(request.environ['PATH_INFO'], '/sub/path/') + self.assertEqual(request.environ['SCRIPT_NAME'], '/path_info/') + class TestStaticURLInfo(unittest.TestCase): def _getTargetClass(self): from pyramid.static import StaticURLInfo @@ -258,6 +334,14 @@ class TestStaticURLInfo(unittest.TestCase): request = DummyRequest() self.assertRaises(ValueError, inst.generate, 'path', request) + def test_generate_registration_miss(self): + inst = self._makeOne(None) + inst.registrations = [('name', 'spec', False), + ('http://example.com/foo/', 'package:path/',True)] + request = DummyRequest() + result = inst.generate('package:path/abc', request) + self.assertEqual(result, 'http://example.com/foo/abc') + def test_generate_slash_in_name1(self): inst = self._makeOne(None) inst.registrations = [('http://example.com/foo/', 'package:path/',True)] @@ -333,6 +417,17 @@ class TestStaticURLInfo(unittest.TestCase): permission='abc') self.assertEqual(config.kw['view_permission'], 'abc') + def test_add_viewname_with_view_permission(self): + class Config: + def add_route(self, *arg, **kw): + self.arg = arg + self.kw = kw + config = Config() + inst = self._makeOne(config) + inst.add('view', 'anotherpackage:path', cache_max_age=1, + view_permission='abc') + self.assertEqual(config.kw['view_permission'], 'abc') + class DummyStartResponse: def __call__(self, status, headerlist, exc_info=None): self.status = status |
