← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~deryck/launchpad/refactor-editemail-doctest-363916 into lp:launchpad

 

Deryck Hodge has proposed merging lp:~deryck/launchpad/refactor-editemail-doctest-363916 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~deryck/launchpad/refactor-editemail-doctest-363916/+merge/103718

This work was done as part of fixing a security issue. In an earlier branch (not yet up for review), I changed Launchpad to re-authenticate a user when going to edit their settings for email, gpg, ssh, etc. The work broke a ton of tests and rather than spend more time than already lost fixing tests, I decided to refactor the page tests into unit tests. I have a number of reasons that I decided to do this, chief of which are:

* It's hard to test in doctest when login redirects to re-auth the user
* I needed to find some LOC credit for the work

This is a large branch deleting doctests, so I decided to land it independent of the actual bug fix.  Everything that was tested in the doctest is either tested already in another place, or I added a new unit tests for it here.  There is no real functionality change here.

We are lint free for these changes, too.
-- 
https://code.launchpad.net/~deryck/launchpad/refactor-editemail-doctest-363916/+merge/103718
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~deryck/launchpad/refactor-editemail-doctest-363916 into lp:launchpad.
=== modified file 'lib/lp/registry/browser/tests/test_person.py'
--- lib/lp/registry/browser/tests/test_person.py	2012-04-17 22:56:29 +0000
+++ lib/lp/registry/browser/tests/test_person.py	2012-04-26 15:25:25 +0000
@@ -3,6 +3,7 @@
 
 __metaclass__ = type
 
+import email
 import doctest
 from textwrap import dedent
 
@@ -16,6 +17,7 @@
     )
 import transaction
 from zope.component import getUtility
+from zope.publisher.interfaces import NotFound
 
 from lp.app.errors import NotFoundError
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
@@ -37,8 +39,11 @@
 from lp.registry.model.milestone import milestone_sort_key
 from lp.services.config import config
 from lp.services.identity.interfaces.account import AccountStatus
+from lp.services.identity.interfaces.emailaddress import IEmailAddressSet
+from lp.services.mail import stub
 from lp.services.verification.interfaces.authtoken import LoginTokenType
 from lp.services.verification.interfaces.logintoken import ILoginTokenSet
+from lp.services.verification.tests.logintoken import get_token_url_from_email
 from lp.services.webapp import canonical_url
 from lp.services.webapp.interfaces import ILaunchBag
 from lp.services.webapp.servers import LaunchpadTestRequest
@@ -65,7 +70,10 @@
     LaunchpadZopelessLayer,
     )
 from lp.testing.matchers import HasQueryCount
-from lp.testing.pages import extract_text
+from lp.testing.pages import (
+    extract_text,
+    setupBrowserForUser,
+    )
 from lp.testing.views import (
     create_initialized_view,
     create_view,
@@ -355,23 +363,57 @@
         self.ppa = self.factory.makeArchive(owner=self.person)
         self.view = create_initialized_view(self.person, '+edit')
 
-    def test_add_email_good_data(self):
-        email_address = self.factory.getUniqueEmailAddress()
+    def createAddEmailView(self, email_address):
+        """Test helper to create +editemails view."""
         form = {
             'field.VALIDATED_SELECTED': self.valid_email_address,
             'field.VALIDATED_SELECTED-empty-marker': 1,
             'field.actions.add_email': 'Add',
             'field.newemail': email_address,
             }
-        create_initialized_view(self.person, "+editemails", form=form)
-
+        return create_initialized_view(self.person, "+editemails", form=form)
+
+    def createSetContactViaAddEmailView(self, email_address):
+        form = {
+            'field.VALIDATED_SELECTED': email_address,
+            'field.actions.set_preferred': 'Set as Contact Address',
+            }
+        return create_initialized_view(self.person, '+editemails', form=form)
+
+    def _assertEmailAndError(self, email_str, expected_msg):
+        view = self.createAddEmailView(email_str)
+        error_msg = view.errors[0]
+        if type(error_msg) != unicode:
+            error_msg = error_msg.doc()
+        self.assertEqual(expected_msg, error_msg)
+
+    def test_add_email(self):
+        stub.test_emails = []
+        email_address = self.factory.getUniqueEmailAddress()
+        view = self.createAddEmailView(email_address)
         # If everything worked, there should now be a login token to validate
         # this email address for this user.
         token = getUtility(ILoginTokenSet).searchByEmailRequesterAndType(
             email_address,
             self.person,
             LoginTokenType.VALIDATEEMAIL)
-        self.assertTrue(token is not None)
+        self.assertIsNotNone(token)
+        notifications = view.request.response.notifications
+        self.assertEqual(1, len(notifications))
+        expected_msg = (
+            u"A confirmation message has been sent to '%s'."
+            " Follow the instructions in that message to confirm"
+            " that the address is yours. (If the message doesn't arrive in a"
+            " few minutes, your mail provider might use 'greylisting', which"
+            " could delay the message for up to an hour or two.)" %
+            email_address)
+        self.assertEqual(expected_msg, notifications[0].message)
+        transaction.commit()
+        self.assertEqual(2, len(stub.test_emails))
+        to_addrs = [to_addr for from_addr, to_addr, msg in stub.test_emails]
+        # Both the new and old addr should be sent email.
+        self.assertIn([self.valid_email_address], to_addrs)
+        self.assertIn([email_address], to_addrs)
 
     def test_add_email_address_taken(self):
         email_address = self.factory.getUniqueEmailAddress()
@@ -380,13 +422,7 @@
             displayname='deadaccount',
             email=email_address,
             account_status=AccountStatus.NOACCOUNT)
-        form = {
-            'field.VALIDATED_SELECTED': self.valid_email_address,
-            'field.VALIDATED_SELECTED-empty-marker': 1,
-            'field.actions.add_email': 'Add',
-            'field.newemail': email_address,
-            }
-        view = create_initialized_view(self.person, "+editemails", form=form)
+        view = self.createAddEmailView(email_address)
         error_msg = view.errors[0]
         expected_msg = (
             "The email address '%s' is already registered to "
@@ -397,6 +433,130 @@
             % email_address)
         self.assertEqual(expected_msg, error_msg)
 
+    def test_validate_email(self):
+        stub.test_emails = []
+        added_email = self.factory.getUniqueEmailAddress()
+        view = self.createAddEmailView(added_email)
+        form = {
+            'field.UNVALIDATED_SELECTED': added_email,
+            'field.actions.validate': 'Confirm',
+            }
+        view = create_initialized_view(self.person, '+editemails', form=form)
+        notifications = view.request.response.notifications
+        self.assertEqual(1, len(notifications))
+        expected_msg = (
+            u"An e-mail message was sent to '%s' "
+            "with instructions on how to confirm that it belongs to you."
+            % added_email)
+        self.assertEqual(expected_msg, notifications[0].message)
+        # Ensure we sent mail to the right address.
+        transaction.commit()
+        to_addrs = [to_addr for from_addr, to_addr, msg in stub.test_emails]
+        self.assertIn([added_email], to_addrs)
+
+    def test_validate_token(self):
+        # Ensure hitting +validateemail actually validates the email.
+        stub.test_emails = []
+        added_email = self.factory.getUniqueEmailAddress()
+        self.createAddEmailView(added_email)
+        form = {
+            'field.UNVALIDATED_SELECTED': added_email,
+            'field.actions.validate': 'Confirm',
+            }
+        create_initialized_view(self.person, '+editemails', form=form)
+        # Get the token from the email msg.
+        transaction.commit()
+        messages = [msg for from_addr, to_addr, msg in stub.test_emails]
+        raw_msg = None
+        for orig_msg in messages:
+            msg = email.message_from_string(orig_msg)
+            if msg.get('to') == added_email:
+                raw_msg = orig_msg
+        token_url = get_token_url_from_email(raw_msg)
+        browser = setupBrowserForUser(user=self.person)
+        browser.open(token_url)
+        expected_msg = u'Confirm e-mail address <code>%s</code>' % added_email
+        self.assertIn(expected_msg, browser.contents)
+        browser.getControl('Continue').click()
+        # Login again to access displayname, since browser logged us out.
+        login_person(self.person)
+        expected_title = u'%s in Launchpad' % self.person.displayname
+        self.assertEqual(expected_title, browser.title)
+
+    def test_remove_unvalidated_email_address(self):
+        added_email = self.factory.getUniqueEmailAddress()
+        view = self.createAddEmailView(added_email)
+        form = {
+            'field.UNVALIDATED_SELECTED': added_email,
+            'field.actions.remove_unvalidated': 'Remove',
+            }
+        view = create_initialized_view(self.person, '+editemails', form=form)
+        notifications = view.request.response.notifications
+        self.assertEqual(1, len(notifications))
+        expected_msg = (
+            u"The email address '%s' has been removed." % added_email)
+        self.assertEqual(expected_msg, notifications[0].message)
+
+    def test_cannot_remove_contact_address(self):
+        form = {
+            'field.VALIDATED_SELECTED': self.valid_email_address,
+            'field.actions.remove_validated': 'Remove',
+            }
+        view = create_initialized_view(self.person, '+editemails', form=form)
+        error_msg = view.errors[0]
+        expected_msg = (
+            "You can't remove %s because it's your contact email address."
+            % self.valid_email_address)
+        self.assertEqual(expected_msg, error_msg)
+
+    def test_set_contact_address(self):
+        added_email = self.factory.getUniqueEmailAddress()
+        view = self.createAddEmailView(added_email)
+        # We need a commit to make sure person and other data are in DB.
+        transaction.commit()
+        validated_email = getUtility(
+            IEmailAddressSet).new(added_email, self.person)
+        self.person.validateAndEnsurePreferredEmail(validated_email)
+        view = self.createSetContactViaAddEmailView(added_email)
+        notifications = view.request.response.notifications
+        self.assertEqual(1, len(notifications))
+        expected_msg = (
+            u"Your contact address has been changed to: %s" % added_email)
+        self.assertEqual(expected_msg, notifications[0].message)
+
+    def test_set_contact_address_already_set(self):
+        view = self.createSetContactViaAddEmailView(self.valid_email_address)
+        notifications = view.request.response.notifications
+        self.assertEqual(1, len(notifications))
+        expected_msg = (
+            "%s is already set as your contact address."
+            % self.valid_email_address)
+        self.assertEqual(expected_msg, notifications[0].message)
+
+    def test_team_editemails_not_found(self):
+        team = self.factory.makeTeam(owner=self.person, members=[self.person])
+        url = '%s/+editemails' % canonical_url(team)
+        browser = setupBrowserForUser(user=self.person)
+        self.assertRaises(NotFound, browser.open, url)
+
+    def test_email_string_validation_no_email_prodvided(self):
+        no_email = ''
+        expected_msg = u'Required input is missing.'
+        self._assertEmailAndError(no_email, expected_msg)
+
+    def test_email_string_validation_invalid_email(self):
+        not_an_email = 'foo'
+        expected_msg = u"'foo' doesn't seem to be a valid email address."
+        self._assertEmailAndError(not_an_email, expected_msg)
+
+    def test_email_string_validation_is_escaped(self):
+        xss_email = "foo@xxxxxxxxxxx<script>window.alert('XSS')</script>"
+        expected_msg = (
+            u"'foo@xxxxxxxxxxx&lt;script&gt;"
+            "window.alert('XSS')&lt;/script&gt;'"
+            " doesn't seem to be a valid email address.")
+        self._assertEmailAndError(xss_email, expected_msg)
+
 
 class PersonAdministerViewTestCase(TestPersonRenameFormMixin,
                                    TestCaseWithFactory):

=== removed file 'lib/lp/registry/stories/person/xx-add-email.txt'
--- lib/lp/registry/stories/person/xx-add-email.txt	2012-04-11 15:48:20 +0000
+++ lib/lp/registry/stories/person/xx-add-email.txt	1970-01-01 00:00:00 +0000
@@ -1,187 +0,0 @@
-= Registering email addresses =
-
-Users can have any number of email addresses registered in their Launchpad
-accounts, although we'll always communicate with them using their preferred
-one. In order to register a new email address users must follow a link sent
-by us to the address they want to add and confirm the registration.
-
-Sample Person will now add a couple email addresses to his account.
-
-    >>> from lp.services.mail import stub
-
-    >>> orig_email = 'test@xxxxxxxxxxxxx'
-    >>> browser = setupBrowser(auth='Basic %s:test' % orig_email)
-    >>> browser.open('http://launchpad.dev/~name12')
-    >>> browser.getLink(url='+editemails').click()
-    >>> browser.url
-    'http://launchpad.dev/~name12/+editemails'
-
-    >>> new_email = 'test2@xxxxxxxxxxxxx'
-    >>> browser.getControl('Add a new address').value = new_email
-    >>> browser.getControl('Add', index=1).click()
-
-    >>> browser.url
-    'http://launchpad.dev/%7Ename12/+editemails'
-    >>> for msg in get_feedback_messages(browser.contents):
-    ...     print msg
-    A confirmation message has been sent to...
-
-There should be an email for the new address and one for the original
-preferred email address that the change was made to their account. Order gets
-mixed up so you have to check both spots.
-
-    >>> to_addrs = [to_addr for from_addr, to_addr, msg in stub.test_emails]
-    >>> assert len(to_addrs) == 2
-    >>> if orig_email in to_addrs[0][0]:
-    ...     assert new_email in to_addrs[1][0]
-    ... else:
-    ...     assert new_email in to_addrs[0][0]
-    ...     assert orig_email in to_addrs[1][0]
-    >>> stub.test_emails = []
-
-Trying to add the same email again (while it's unconfirmed) will only cause
-a new email to be sent with a new link. The links in the old and new email are
-still accessible and will confirm the new address if/when the user follows
-them.
-
-    >>> browser.getControl('Add a new address').value = 'test2@xxxxxxxxxxxxx'
-    >>> browser.getControl('Add', index=1).click()
-
-    >>> browser.url
-    'http://launchpad.dev/%7Ename12/+editemails'
-    >>> for msg in get_feedback_messages(browser.contents):
-    ...     print msg
-    A confirmation message has been sent to...
-
-    # Extract the link (from the email we just sent) the user will have to
-    # use to finish the registration process. There will be two emails so we
-    # must check them both in order to make sure it's sent.
-    >>> from lp.services.verification.tests.logintoken import (
-    ...     get_token_url_from_email)
-    >>> def check_tokenemail_and_reset():
-    ...     found, url, toaddr = False, None, None
-    ...     for from_addr, to_addrs, raw_msg in stub.test_emails:
-    ...         if 'token' in raw_msg:
-    ...             found = True
-    ...             url = get_token_url_from_email(raw_msg)
-    ...             toaddr = to_addrs
-    ...     assert found
-    ...     return url, toaddr
-    >>> token_url, toaddrs = check_tokenemail_and_reset()
-    >>> token_url
-    'http://launchpad.dev/token/...'
-    >>> toaddrs
-    ['test2@xxxxxxxxxxxxx']
-
-Follow the token link, to confirm the new email address.
-
-    >>> browser.open(token_url)
-    >>> browser.url
-    'http://launchpad.dev/token/.../+validateemail'
-    >>> browser.getControl('Continue').click()
-
-    >>> browser.url
-    'http://launchpad.dev/~name12'
-    >>> for msg in get_feedback_messages(browser.contents):
-    ...     print msg
-    Email address successfully confirmed.
-
-Now that the address is confirmed he sees it in the list of his confirmed
-addresses.
-
-    >>> from lp.testing.pages import strip_label
-
-    >>> browser.getLink(url='+editemails').click()
-    >>> confirmed = browser.getControl(name="field.VALIDATED_SELECTED")
-    >>> [strip_label(option) for option in confirmed.displayOptions]
-    ['test@xxxxxxxxxxxxx', 'test2@xxxxxxxxxxxxx', 'testing@xxxxxxxxxxxxx']
-
-If he tries to add it again, he'll get an error message explaining
-that it's already registered.
-
-    >>> browser.getControl('Add a new address').value = 'test2@xxxxxxxxxxxxx'
-    >>> browser.getControl('Add', index=1).click()
-    >>> browser.url
-    'http://launchpad.dev/%7Ename12/+editemails'
-    >>> for msg in get_feedback_messages(browser.contents):
-    ...     print msg
-    There is 1 error.
-    The email address 'test2@xxxxxxxxxxxxx' is already registered as your
-    email address...
-
-
-== Adding a second email address ==
-
-    >>> browser.getControl('Add a new address').value = 'sample@xxxxxxxxxx'
-    >>> browser.getControl('Add', index=1).click()
-
-    >>> browser.url
-    'http://launchpad.dev/%7Ename12/+editemails'
-    >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     print tag.renderContents()
-    A confirmation message has been sent to...
-
-    # Extract the link (from the email we just sent) the user will have to
-    # use to finish the registration process.
-    >>> token_url, toaddrs = check_tokenemail_and_reset()
-    >>> token_url
-    'http://launchpad.dev/token/...'
-    >>> toaddrs
-    ['sample@xxxxxxxxxx']
-
-Follow the token link, to confirm the new email address.
-
-    >>> browser.open(token_url)
-    >>> browser.url
-    'http://launchpad.dev/token/.../+validateemail'
-    >>> browser.getControl('Continue').click()
-
-    >>> browser.url
-    'http://launchpad.dev/~name12'
-
-    >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     print tag.renderContents()
-    Email address successfully confirmed.
-
-
-== Trying to register an already registered email address ==
-
-Email addresses can not be registered more than once in Launchpad, so if
-a given email address is registered by Joe, then Sample Person won't be
-able to register it for himself.  When that happens we tell the user the
-email is already registered and explain he may want to merge the other
-account if that's a duplicate.
-
-    >>> login(ANONYMOUS)
-    >>> person = factory.makePerson(email='joe@xxxxxxxxxx')
-    >>> person_with_hidden_emails = factory.makePerson(
-    ...     email='joe2@xxxxxxxxxx', hide_email_addresses=True)
-    >>> logout()
-    >>> browser.open('http://launchpad.dev/~name12/+editemails')
-    >>> browser.getControl('Add a new address').value = 'joe@xxxxxxxxxx'
-    >>> browser.getControl('Add', index=1).click()
-    >>> print "\n".join(get_feedback_messages(browser.contents))
-    There is 1 error.
-    The email address 'joe@xxxxxxxxxx' is already registered...
-    If you think that is a duplicated account, you can merge it...
-
-    >>> browser.open('http://launchpad.dev/~name12/+editemails')
-    >>> browser.getControl('Add a new address').value = 'joe2@xxxxxxxxxx'
-    >>> browser.getControl('Add', index=1).click()
-    >>> print "\n".join(get_feedback_messages(browser.contents))
-    There is 1 error.
-    The email address 'joe2@xxxxxxxxxx' is already registered...
-    If you think that is a duplicated account, you can merge it...
-
-If someone tries to add an already registered email address for a team,
-a similar error will be shown.
-
-    >>> browser.open(
-    ...     'http://launchpad.dev/~landscape-developers/+contactaddress')
-    >>> browser.getControl('Another e-mail address').selected = True
-    >>> browser.getControl(
-    ...     name='field.contact_address').value = 'joe2@xxxxxxxxxx'
-    >>> browser.getControl('Change').click()
-    >>> print "\n".join(get_feedback_messages(browser.contents))
-    There is 1 error.
-    joe2@xxxxxxxxxx is already registered in Launchpad...

=== removed file 'lib/lp/registry/stories/person/xx-person-delete-email.txt'
--- lib/lp/registry/stories/person/xx-person-delete-email.txt	2008-08-28 13:19:44 +0000
+++ lib/lp/registry/stories/person/xx-person-delete-email.txt	1970-01-01 00:00:00 +0000
@@ -1,23 +0,0 @@
-= Removing an email address =
-
-Any email address other than the preferred one can be removed from
-a person's +editemails page.
-
-    >>> browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
-    >>> browser.open('http://launchpad.dev/~name12/+editemails')
-    >>> browser.getControl('test@xxxxxxxxxxxxx').selected = True
-    >>> browser.getControl('Remove', index=0).click()
-    >>> print "\n".join(get_feedback_messages(browser.contents))
-    There is 1 error.
-    You can't remove test@xxxxxxxxxxxxx because it's your contact email
-    address.
-
-    >>> browser.getControl('testing@xxxxxxxxxxxxx').selected = True
-    >>> browser.getControl('Remove', index=0).click()
-    >>> print "\n".join(get_feedback_messages(browser.contents))
-    The email address 'testing@xxxxxxxxxxxxx' has been removed.
-
-    >>> browser.getControl('testing@xxxxxxxxxxxxx')
-    Traceback (most recent call last):
-    ...
-    LookupError: label 'testing@xxxxxxxxxxxxx'

=== removed file 'lib/lp/registry/stories/person/xx-set-preferredemail.txt'
--- lib/lp/registry/stories/person/xx-set-preferredemail.txt	2010-06-18 23:25:36 +0000
+++ lib/lp/registry/stories/person/xx-set-preferredemail.txt	1970-01-01 00:00:00 +0000
@@ -1,64 +0,0 @@
-================================
-A person's contact email address
-================================
-
-A person may have many confirmed email address which he may use to
-login with, but only one email address will be the contact email
-address.
-
-
-Setting the contact email address
-=================================
-
-Sample Person chooses to change his contact email address to the
-address he prefers to login with.
-
-    >>> browser = setupBrowser(auth='Basic testing@xxxxxxxxxxxxx:test')
-    >>> browser.open('http://launchpad.dev/~name12')
-    >>> browser.getLink(url='+editemails').click()
-    >>> print browser.url
-    http://launchpad.dev/~name12/+editemails
-    >>> print browser.title
-    Change your e-mail settings...
-
-Sample Person has a second browser open to the same page; maybe he is
-absent minded.
-
-    >>> second_browser = setupBrowser(auth='Basic testing@xxxxxxxxxxxxx:test')
-    >>> second_browser.open('http://launchpad.dev/~name12/+editemails')
-
-His confirmed email addresses are listed for him to manage. His
-contact email address is selected, and always first.
-
-    >>> print_radio_button_field(browser.contents, "VALIDATED_SELECTED")
-    (*) test@xxxxxxxxxxxxx
-    ( ) testing@xxxxxxxxxxxxx
-
-Sample Persons selects his testing email address and submits his choice
-with the "Set as Contact Address" button.
-
-    >>> browser.getControl('testing@xxxxxxxxxxxxx').selected = True
-    >>> browser.getControl('Set as Contact Address').click()
-
-    >>> for msg in get_feedback_messages(browser.contents):
-    ...     print msg
-    Your contact address has been changed to: testing@xxxxxxxxxxxxx
-
-His testing email address is now first in the list and selected
-as his contact address.
-
-    >>> print_radio_button_field(browser.contents, "VALIDATED_SELECTED")
-    (*) testing@xxxxxxxxxxxxx
-    ( ) test@xxxxxxxxxxxxx
-
-In a moment of deja vu, Sample Person returns to his second browser,
-selects his testing email address again, then submits his choice. There
-is no change; he sees a message explaining that testing was already his
-contact address.
-
-    >>> second_browser.getControl('testing@xxxxxxxxxxxxx').selected = True
-    >>> second_browser.getControl('Set as Contact Address').click()
-
-    >>> for msg in get_feedback_messages(second_browser.contents):
-    ...     print msg
-    testing@xxxxxxxxxxxxx is already set as your contact address.

=== removed file 'lib/lp/registry/stories/person/xx-validate-email.txt'
--- lib/lp/registry/stories/person/xx-validate-email.txt	2012-04-11 14:40:02 +0000
+++ lib/lp/registry/stories/person/xx-validate-email.txt	1970-01-01 00:00:00 +0000
@@ -1,150 +0,0 @@
-===========================
-Validating an email address
-===========================
-
-The user 'salgado' has an unvalidated email address that was probably
-added by gina. Now he wants to validate it.
-
-    # Workaround for https://launchpad.net/launchpad/+bug/39016
-    >>> from lp.services.mail import stub
-    >>> stub.test_emails[:] = []
-
-    >>> browser = setupBrowser(auth='Basic salgado@xxxxxxxxxx:test')
-    >>> browser.open('http://launchpad.dev/~salgado/+editemails')
-    >>> print browser.title
-    Change your e-mail settings...
-
-    >>> browser.getControl(name="field.UNVALIDATED_SELECTED").getControl(
-    ...     value='salgado@xxxxxxxxxx').selected = True
-    >>> browser.getControl('Confirm').click()
-    >>> for msg in get_feedback_messages(browser.contents):
-    ...     print msg
-    An e-mail message was sent to 'salgado@xxxxxxxxxx'...
-
-Retrieve the email and make sure it was sent to the right address.
-
-    >>> import email
-    >>> found = False
-    >>> raw_msg = None
-    >>> for from_addr, to_addrs, orig_msg in stub.test_emails:
-    ...     msg = email.message_from_string(orig_msg)
-    ...     if msg.get('to') == 'salgado@xxxxxxxxxx':
-    ...        raw_msg = orig_msg
-    ...        found = True
-    >>> assert found
-
-Visit the token URL mentioned in the email, and get redirected to
-+validateemail.
-
-    >>> from lp.services.verification.tests.logintoken import (
-    ...     get_token_url_from_email)
-    >>> token_url = get_token_url_from_email(raw_msg)
-    >>> browser.open(token_url)
-
-    >>> print browser.url
-    http://launchpad.dev/token/.../+validateemail
-    >>> print browser.title
-    Confirm e-mail address
-
-    >>> print extract_text(find_main_content(browser.contents))
-    Confirm e-mail address
-    Confirm e-mail address salgado@xxxxxxxxxx
-
-    >>> browser.getControl('Continue').click()
-    >>> print browser.title
-    Guilherme Salgado in Launchpad
-
-Check that the email address now shows up as validated.
-
-    >>> browser.getLink(url='+editemails').click()
-    >>> browser.getControl(name="field.VALIDATED_SELECTED").getControl(
-    ...     value='salgado@xxxxxxxxxx')
-    <ItemControl...optionValue='salgado@xxxxxxxxxx'...>
-
-    >>> browser.getControl(name="field.UNVALIDATED_SELECTED").getControl(
-    ...     value='salgado@xxxxxxxxxx')
-    Traceback (most recent call last):
-    ...
-    LookupError...
-
-An email address that the user adds manually should have the same
-validation workflow as an email address guessed by the system and
-added by gina.
-
-    >>> browser.getControl('Add a new address').value = 'salgado@xxxxxxxxxxx'
-    >>> browser.getControl('Add', index=1).click()
-    >>> browser.url
-    'http://launchpad.dev/%7Esalgado/+editemails'
-
-    >>> browser.getControl(name="field.UNVALIDATED_SELECTED").getControl(
-    ...     value='salgado@xxxxxxxxxxx').selected = True
-    >>> browser.getControl('Confirm').click()
-    >>> for msg in get_feedback_messages(browser.contents):
-    ...     print msg
-    An e-mail message was sent to 'salgado@xxxxxxxxxxx'...
-
-
-Validating the email address string
-===================================
-
-Leaving the email address field blank and hitting the 'Add' button
-should display an error message.
-
-    >>> browser.getControl('Add a new address').value
-    ''
-
-    >>> browser.getControl('Add', index=1).click()
-    >>> print browser.title
-    Change your e-mail settings...
-
-    >>> for msg in get_feedback_messages(browser.contents):
-    ...     print msg
-    There is 1 error.
-    Required input is missing.
-
-Entering a string that does not look like an email address causes an
-error to be displayed.
-
-    >>> print browser.title
-    Change your e-mail settings...
-
-    >>> browser.getControl('Add a new address').value = 'foo'
-    >>> browser.getControl('Add', index=1).click()
-    >>> print browser.title
-    Change your e-mail settings...
-
-    >>> for msg in get_feedback_messages(browser.contents):
-    ...     print msg
-    There is 1 error.
-    'foo' doesn't seem to be a valid email address.
-
-Markup in new addresses is escaped in error messages so that it cannot
-be interpreted by the browser. Scripts that are embedded in the address
-will not run. A malicious hacker cannot use a XSS vulnerability to gain
-information about Launchpad users.
-
-    >>> print browser.title
-    Change your e-mail settings...
-
-    >>> browser.getControl('Add a new address').value = (
-    ...     "salgado@xxxxxxxxxxx<br/><script>window.alert('XSS')</script>")
-    >>> browser.getControl('Add', index=1).click()
-    >>> for msg in get_feedback_messages(browser.contents):
-    ...     print msg
-    There is 1 error.
-    'salgado...com&lt;br/&gt;&lt;script&gt;window.alert('XSS')&lt;/script&gt;'
-    doesn't seem to be a valid email address.
-
-
-+editemails for teams
-=====================
-
-Because people and teams share the same namespace, it used to be possible to
-hack the url to get to a team's +editemails page.  This makes no sense, and
-for teams you should use the +contactaddress url.  The +editemails page is no
-longer available for teams.
-
-    >>> browser.open('http://launchpad.dev/~guadamen/+editemails')
-    Traceback (most recent call last):
-    ...
-    NotFound: Object: <...>, name: u'+editemails'


Follow ups