summaryrefslogtreecommitdiff
path: root/docs/tutorials/wiki
diff options
context:
space:
mode:
Diffstat (limited to 'docs/tutorials/wiki')
-rw-r--r--docs/tutorials/wiki/authorization.rst218
-rw-r--r--docs/tutorials/wiki/definingmodels.rst70
-rw-r--r--docs/tutorials/wiki/definingviews.rst45
-rw-r--r--docs/tutorials/wiki/index.rst1
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/tests.py216
-rw-r--r--docs/tutorials/wiki/tests.rst78
6 files changed, 403 insertions, 225 deletions
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/docs/tutorials/wiki/definingmodels.rst b/docs/tutorials/wiki/definingmodels.rst
index 3d2d01061..baf497458 100644
--- a/docs/tutorials/wiki/definingmodels.rst
+++ b/docs/tutorials/wiki/definingmodels.rst
@@ -89,70 +89,16 @@ something like this:
:linenos:
:language: python
-Removing View Configuration
----------------------------
-
-In a previous step in this chapter, we removed the
-``tutorial.models.MyModel`` class. However, our ``views.py`` module still
-attempts to import this class. Temporarily, we'll change ``views.py`` so
-that it no longer references ``MyModel`` by removing its imports and removing
-a reference to it from the arguments passed to the ``@view_config``
-:term:`configuration decoration` decorator which sits atop the ``my_view``
-view callable.
-
-The result of all of our edits to ``views.py`` will end up looking
-something like this:
-
-.. literalinclude:: src/models/tutorial/views.py
- :linenos:
- :language: python
-
-Testing the Models
-------------------
-
-To make sure the code we just wrote works, we write tests for the model
-classes and the appmaker. Changing ``tests.py``, we'll write a separate test
-class for each model class, and we'll write a test class for the
-``appmaker``.
-
-To do so, we'll retain the ``tutorial.tests.ViewTests`` class provided as a
-result of the ``pyramid_zodb`` project generator. We'll add three test
-classes: one for the ``Page`` model named ``PageModelTests``, one for the
-``Wiki`` model named ``WikiModelTests``, and one for the appmaker named
-``AppmakerTests``.
-
-When we're done changing ``tests.py``, it will look something like so:
-
-.. literalinclude:: src/models/tutorial/tests.py
- :linenos:
- :language: python
+Viewing the Application in a Browser
+------------------------------------
-Running the Tests
------------------
-
-We can run these tests by using ``setup.py test`` in the same way we
-did in :ref:`running_tests`. Assuming our shell's current working
-directory is the "tutorial" distribution directory:
-
-On UNIX:
-
-.. code-block:: text
-
- $ ../bin/python setup.py test -q
-
-On Windows:
-
-.. code-block:: text
-
- c:\pyramidtut\tutorial> ..\Scripts\python setup.py test -q
-
-The expected output is something like this:
+We can't. At this point, our system is in a "non-runnable" state; we'll need
+to change view-related files in the next chapter to be able to start the
+application successfully. If you try to start the application, you'll wind
+up with a Python traceback on your console that ends with this exception:
.. code-block:: text
- .....
- ----------------------------------------------------------------------
- Ran 5 tests in 0.008s
-
- OK
+ ImportError: cannot import name MyModel
+This will also happen if you attempt to run the tests.
diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst
index 233e571f1..b6c083bbf 100644
--- a/docs/tutorials/wiki/definingviews.rst
+++ b/docs/tutorials/wiki/definingviews.rst
@@ -318,48 +318,3 @@ browser. The views we'll try are as follows:
will generate an ``IndexError`` for the expression
``request.subpath[0]``. You'll see an interactive traceback
facility provided by :term:`WebError`.
-
-Testing the Views
-=================
-
-We'll modify our ``tests.py`` file, adding tests for each view function we
-added above. As a result, we'll *delete* the ``ViewTests`` test in the file,
-and add four other test classes: ``ViewWikiTests``, ``ViewPageTests``,
-``AddPageTests``, and ``EditPageTests``. These test the ``view_wiki``,
-``view_page``, ``add_page``, and ``edit_page`` views respectively.
-
-Once we're done with the ``tests.py`` module, it will look a lot like the
-below:
-
-.. literalinclude:: src/views/tutorial/tests.py
- :linenos:
- :language: python
-
-Running the Tests
-=================
-
-We can run these tests by using ``setup.py test`` in the same way we did in
-:ref:`running_tests`. Assuming our shell's current working directory is the
-"tutorial" distribution directory:
-
-On UNIX:
-
-.. code-block:: text
-
- $ ../bin/python setup.py test -q
-
-On Windows:
-
-.. code-block:: text
-
- c:\pyramidtut\tutorial> ..\Scripts\python setup.py test -q
-
-The expected result looks something like:
-
-.. code-block:: text
-
- .........
- ----------------------------------------------------------------------
- Ran 9 tests in 0.203s
-
- OK
diff --git a/docs/tutorials/wiki/index.rst b/docs/tutorials/wiki/index.rst
index 660bf3bd3..c984c4f01 100644
--- a/docs/tutorials/wiki/index.rst
+++ b/docs/tutorials/wiki/index.rst
@@ -23,5 +23,6 @@ tutorial can be browsed at
definingmodels
definingviews
authorization
+ tests
distributing
diff --git a/docs/tutorials/wiki/src/tests/tutorial/tests.py b/docs/tutorials/wiki/src/tests/tutorial/tests.py
new file mode 100644
index 000000000..d9ff866f1
--- /dev/null
+++ b/docs/tutorials/wiki/src/tests/tutorial/tests.py
@@ -0,0 +1,216 @@
+import unittest
+
+from pyramid import testing
+
+class PageModelTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from tutorial.models import Page
+ return Page
+
+ def _makeOne(self, data=u'some data'):
+ return self._getTargetClass()(data=data)
+
+ def test_constructor(self):
+ instance = self._makeOne()
+ self.assertEqual(instance.data, u'some data')
+
+class WikiModelTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from tutorial.models import Wiki
+ return Wiki
+
+ def _makeOne(self):
+ return self._getTargetClass()()
+
+ def test_it(self):
+ wiki = self._makeOne()
+ self.assertEqual(wiki.__parent__, None)
+ self.assertEqual(wiki.__name__, None)
+
+class AppmakerTests(unittest.TestCase):
+ def _callFUT(self, zodb_root):
+ from tutorial.models import appmaker
+ return appmaker(zodb_root)
+
+ def test_it(self):
+ root = {}
+ self._callFUT(root)
+ self.assertEqual(root['app_root']['FrontPage'].data,
+ 'This is the front page')
+
+class ViewWikiTests(unittest.TestCase):
+ def test_it(self):
+ from tutorial.views import view_wiki
+ context = testing.DummyResource()
+ request = testing.DummyRequest()
+ response = view_wiki(context, request)
+ self.assertEqual(response.location, 'http://example.com/FrontPage')
+
+class ViewPageTests(unittest.TestCase):
+ def _callFUT(self, context, request):
+ from tutorial.views import view_page
+ return view_page(context, request)
+
+ def test_it(self):
+ wiki = testing.DummyResource()
+ wiki['IDoExist'] = testing.DummyResource()
+ context = testing.DummyResource(data='Hello CruelWorld IDoExist')
+ context.__parent__ = wiki
+ context.__name__ = 'thepage'
+ request = testing.DummyRequest()
+ info = self._callFUT(context, request)
+ self.assertEqual(info['page'], context)
+ self.assertEqual(
+ info['content'],
+ '<div class="document">\n'
+ '<p>Hello <a href="http://example.com/add_page/CruelWorld">'
+ 'CruelWorld</a> '
+ '<a href="http://example.com/IDoExist/">'
+ 'IDoExist</a>'
+ '</p>\n</div>\n')
+ self.assertEqual(info['edit_url'],
+ 'http://example.com/thepage/edit_page')
+
+
+class AddPageTests(unittest.TestCase):
+ def _callFUT(self, context, request):
+ from tutorial.views import add_page
+ return add_page(context, request)
+
+ def test_it_notsubmitted(self):
+ from pyramid.url import resource_url
+ context = testing.DummyResource()
+ request = testing.DummyRequest()
+ request.subpath = ['AnotherPage']
+ info = self._callFUT(context, request)
+ self.assertEqual(info['page'].data,'')
+ self.assertEqual(
+ info['save_url'],
+ resource_url(context, request, 'add_page', 'AnotherPage'))
+
+ def test_it_submitted(self):
+ context = testing.DummyResource()
+ request = testing.DummyRequest({'form.submitted':True,
+ 'body':'Hello yo!'})
+ request.subpath = ['AnotherPage']
+ self._callFUT(context, request)
+ page = context['AnotherPage']
+ self.assertEqual(page.data, 'Hello yo!')
+ self.assertEqual(page.__name__, 'AnotherPage')
+ self.assertEqual(page.__parent__, context)
+
+class EditPageTests(unittest.TestCase):
+ def _callFUT(self, context, request):
+ from tutorial.views import edit_page
+ return edit_page(context, request)
+
+ def test_it_notsubmitted(self):
+ from pyramid.url import resource_url
+ context = testing.DummyResource()
+ request = testing.DummyRequest()
+ info = self._callFUT(context, request)
+ self.assertEqual(info['page'], context)
+ self.assertEqual(info['save_url'],
+ resource_url(context, request, 'edit_page'))
+
+ def test_it_submitted(self):
+ context = testing.DummyResource()
+ request = testing.DummyRequest({'form.submitted':True,
+ 'body':'Hello yo!'})
+ response = self._callFUT(context, request)
+ self.assertEqual(response.location, 'http://example.com/')
+ self.assertEqual(context.data, 'Hello yo!')
+
+class FunctionalTests(unittest.TestCase):
+
+ viewer_login = '/login?login=viewer&password=viewer' \
+ '&came_from=FrontPage&form.submitted=Login'
+ viewer_wrong_login = '/login?login=viewer&password=incorrect' \
+ '&came_from=FrontPage&form.submitted=Login'
+ editor_login = '/login?login=editor&password=editor' \
+ '&came_from=FrontPage&form.submitted=Login'
+
+ def setUp(self):
+ import tempfile
+ import os.path
+ from tutorial import main
+ self.tmpdir = tempfile.mkdtemp()
+
+ dbpath = os.path.join( self.tmpdir, 'test.db')
+ settings = { 'zodb_uri' : 'file://' + dbpath }
+
+ app = main({}, **settings)
+ from repoze.zodbconn.middleware import EnvironmentDeleterMiddleware
+ app = EnvironmentDeleterMiddleware(app)
+ from webtest import TestApp
+ self.testapp = TestApp(app)
+
+ def tearDown(self):
+ import shutil
+ shutil.rmtree( self.tmpdir )
+
+ def test_root(self):
+ res = self.testapp.get('/', status=302)
+ self.assertTrue(not res.body)
+
+ def test_FrontPage(self):
+ res = self.testapp.get('/FrontPage', status=200)
+ self.assertTrue('FrontPage' in res.body)
+
+ def test_unexisting_page(self):
+ res = self.testapp.get('/SomePage', status=404)
+ self.assertTrue('Not Found' in res.body)
+
+ def test_successful_log_in(self):
+ res = self.testapp.get( self.viewer_login, status=302)
+ self.assertTrue(res.location == 'FrontPage')
+
+ def test_failed_log_in(self):
+ res = self.testapp.get( self.viewer_wrong_login, status=200)
+ self.assertTrue('login' in res.body)
+
+ def test_logout_link_present_when_logged_in(self):
+ res = self.testapp.get( self.viewer_login, status=302)
+ res = self.testapp.get('/FrontPage', status=200)
+ self.assertTrue('Logout' in res.body)
+
+ def test_logout_link_not_present_after_logged_out(self):
+ res = self.testapp.get( self.viewer_login, status=302)
+ res = self.testapp.get('/FrontPage', status=200)
+ res = self.testapp.get('/logout', status=302)
+ self.assertTrue('Logout' not in res.body)
+
+ def test_anonymous_user_cannot_edit(self):
+ res = self.testapp.get('/FrontPage/edit_page', status=200)
+ self.assertTrue('Login' in res.body)
+
+ def test_anonymous_user_cannot_add(self):
+ res = self.testapp.get('/add_page/NewPage', status=200)
+ self.assertTrue('Login' in res.body)
+
+ def test_viewer_user_cannot_edit(self):
+ res = self.testapp.get( self.viewer_login, status=302)
+ res = self.testapp.get('/FrontPage/edit_page', status=200)
+ self.assertTrue('Login' in res.body)
+
+ def test_viewer_user_cannot_add(self):
+ res = self.testapp.get( self.viewer_login, status=302)
+ res = self.testapp.get('/add_page/NewPage', status=200)
+ self.assertTrue('Login' in res.body)
+
+ def test_editors_member_user_can_edit(self):
+ res = self.testapp.get( self.editor_login, status=302)
+ res = self.testapp.get('/FrontPage/edit_page', status=200)
+ self.assertTrue('Editing' in res.body)
+
+ def test_editors_member_user_can_add(self):
+ res = self.testapp.get( self.editor_login, status=302)
+ res = self.testapp.get('/add_page/NewPage', status=200)
+ self.assertTrue('Editing' in res.body)
+
+ def test_editors_member_user_can_view(self):
+ res = self.testapp.get( self.editor_login, status=302)
+ res = self.testapp.get('/FrontPage', status=200)
+ self.assertTrue('FrontPage' in res.body)
diff --git a/docs/tutorials/wiki/tests.rst b/docs/tutorials/wiki/tests.rst
new file mode 100644
index 000000000..f3151dbcc
--- /dev/null
+++ b/docs/tutorials/wiki/tests.rst
@@ -0,0 +1,78 @@
+============
+Adding Tests
+============
+
+We will now add tests for the models and the views and a few functional
+tests in the ``tests.py``. Tests ensure that an application works, and
+that it continues to work after some changes are made in the future.
+
+Testing the Models
+==================
+
+We write tests for the model
+classes and the appmaker. Changing ``tests.py``, we'll write a separate test
+class for each model class, and we'll write a test class for the
+``appmaker``.
+
+To do so, we'll retain the ``tutorial.tests.ViewTests`` class provided as a
+result of the ``pyramid_zodb`` project generator. We'll add three test
+classes: one for the ``Page`` model named ``PageModelTests``, one for the
+``Wiki`` model named ``WikiModelTests``, and one for the appmaker named
+``AppmakerTests``.
+
+Testing the Views
+=================
+
+We'll modify our ``tests.py`` file, adding tests for each view function we
+added above. As a result, we'll *delete* the ``ViewTests`` test in the file,
+and add four other test classes: ``ViewWikiTests``, ``ViewPageTests``,
+``AddPageTests``, and ``EditPageTests``. These test the ``view_wiki``,
+``view_page``, ``add_page``, and ``edit_page`` views respectively.
+
+
+Functional tests
+================
+
+We test the whole application, covering security aspects that are not
+tested in the unit tests, like logging in, logging out, checking that
+the ``viewer`` user cannot add or edit pages, but the ``editor`` user
+can, and so on.
+
+Viewing the results of all our edits to ``tests.py``
+====================================================
+
+Once we're done with the ``tests.py`` module, it will look a lot like the
+below:
+
+.. literalinclude:: src/tests/tutorial/tests.py
+ :linenos:
+ :language: python
+
+Running the Tests
+=================
+
+We can run these tests by using ``setup.py test`` in the same way we did in
+:ref:`running_tests`. Assuming our shell's current working directory is the
+"tutorial" distribution directory:
+
+On UNIX:
+
+.. code-block:: text
+
+ $ ../bin/python setup.py test -q
+
+On Windows:
+
+.. code-block:: text
+
+ c:\pyramidtut\tutorial> ..\Scripts\python setup.py test -q
+
+The expected result looks something like:
+
+.. code-block:: text
+
+ .........
+ ----------------------------------------------------------------------
+ Ran 9 tests in 0.203s
+
+ OK