import re
import cherrypy
import smtplib
import urllib
from pytz import utc
from nose.tools import raises
from datetime import datetime, timedelta
from Test_controller import Test_controller
from Stub_smtp import Stub_smtp
import Stub_urllib2
from model.User import User
from model.Notebook import Notebook
from model.Note import Note
from model.Password_reset import Password_reset
from model.Invite import Invite
from controller.Users import Invite_error, Payment_error
import controller.Users as Users


class Test_users( Test_controller ):
  RESET_LINK_PATTERN = re.compile( "(https?://\S+)?/r/(\S+)" )
  INVITE_LINK_PATTERN = re.compile( "(https?://\S+)?/i/(\S+)" )

  def setUp( self ):
    Test_controller.setUp( self )
    Users.urllib2 = Stub_urllib2

    self.username = u"mulder"
    self.password = u"trustno1"
    self.email_address = u"out-there@example.com"
    self.new_username = u"reynolds"
    self.new_password = u"shiny"
    self.new_email_address = u"capn@example.com"
    self.username2 = u"scully"
    self.password2 = u"trustsome1"
    self.email_address2 = u"out-there@example.com"
    self.user = None
    self.user2 = None
    self.anonymous = None
    self.notebooks = None
    self.session_id = None

    self.make_users()

  def make_users( self ):
    notebook_id1 = self.database.next_id( Notebook )
    notebook_id2 = self.database.next_id( Notebook )
    trash_id1 = self.database.next_id( Notebook )
    trash_id2 = self.database.next_id( Notebook )

    self.database.save( Notebook.create( trash_id1, u"trash" ) )
    self.database.save( Notebook.create( trash_id2, u"trash" ) )

    self.notebooks = [
      Notebook.create( notebook_id1, u"my notebook", trash_id = trash_id1 ),
      Notebook.create( notebook_id2, u"my other notebook", trash_id = trash_id2 ),
    ]
    self.database.save( self.notebooks[ 0 ] )
    self.database.save( self.notebooks[ 1 ] )

    self.anon_notebook = Notebook.create( self.database.next_id( Notebook ), u"anon notebook" )
    self.database.save( self.anon_notebook )
    self.startup_note = Note.create(
      self.database.next_id( Note ), u"<h3>login</h3>",
      notebook_id = self.anon_notebook.object_id, startup = True,
    )
    self.database.save( self.startup_note )

    self.user = User.create( self.database.next_id( User ), self.username, self.password, self.email_address )
    self.database.save( self.user, commit = False )
    self.database.execute( self.user.sql_save_notebook( notebook_id1, read_write = True, owner = True, rank = 0 ), commit = False )
    self.database.execute( self.user.sql_save_notebook( trash_id1, read_write = True, owner = True ), commit = False )
    self.database.execute( self.user.sql_save_notebook( notebook_id2, read_write = True, owner = True, rank = 1 ), commit = False )
    self.database.execute( self.user.sql_save_notebook( trash_id2, read_write = True, owner = True ), commit = False )

    self.user2 = User.create( self.database.next_id( User ), self.username2, self.password2, self.email_address2 )
    self.database.save( self.user2, commit = False )

    self.anonymous = User.create( self.database.next_id( User ), u"anonymous" )
    self.database.save( self.anonymous, commit = False )
    self.database.execute( self.anonymous.sql_save_notebook( self.anon_notebook.object_id ), commit = False )

    self.database.commit()

  def test_signup( self ):
    result = self.http_post( "/users/signup", dict(
      username = self.new_username,
      password = self.new_password,
      password_repeat = self.new_password,
      email_address = self.new_email_address,
      signup_button = u"sign up",
    ) )

    assert result[ u"redirect" ].startswith( u"/notebooks/" )

  def test_signup_with_rate_plan( self ):
    result = self.http_post( "/users/signup", dict(
      username = self.new_username,
      password = self.new_password,
      password_repeat = self.new_password,
      email_address = self.new_email_address,
      signup_button = u"sign up",
      rate_plan = u"2",
    ) )

    assert result[ u"redirect" ] == u"/users/subscribe?rate_plan=2"

  def test_signup_without_email_address( self ):
    result = self.http_post( "/users/signup", dict(
      username = self.new_username,
      password = self.new_password,
      password_repeat = self.new_password,
      email_address = u"",
      signup_button = u"sign up",
    ) )

    assert result[ u"redirect" ].startswith( u"/notebooks/" )

  def test_signup_with_invalid_email_address( self ):
    result = self.http_post( "/users/signup", dict(
      username = self.new_username,
      password = self.new_password,
      password_repeat = self.new_password,
      email_address = u"foo@",
      signup_button = u"sign up",
    ) )

    assert u"error" in result

  def test_current_after_signup( self ):
    result = self.http_post( "/users/signup", dict(
      username = self.new_username,
      password = self.new_password,
      password_repeat = self.new_password,
      email_address = self.new_email_address,
      signup_button = u"sign up",
    ) )
    session_id = result[ u"session_id" ]

    new_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ]

    user = self.database.last_saved_obj
    assert isinstance( user, User )
    result = cherrypy.root.users.current( user.object_id )

    assert result[ u"user" ].object_id == user.object_id
    assert result[ u"user" ].username == self.new_username
    assert result[ u"user" ].email_address == self.new_email_address

    notebooks = result[ u"notebooks" ]
    notebook = notebooks[ 0 ]
    assert notebook.object_id == new_notebook_id
    assert notebook.revision
    assert notebook.name == u"my notebook"
    assert notebook.trash_id
    assert notebook.read_write == True
    assert notebook.owner == True
    assert notebook.rank == 0

    notebook = notebooks[ 1 ]
    assert notebook.object_id == notebooks[ 0 ].trash_id
    assert notebook.revision
    assert notebook.name == u"trash"
    assert notebook.trash_id == None
    assert notebook.read_write == True
    assert notebook.owner == True
    assert notebook.rank == None

    notebook = notebooks[ 2 ]
    assert notebook.object_id == self.anon_notebook.object_id
    assert notebook.revision == self.anon_notebook.revision
    assert notebook.name == self.anon_notebook.name
    assert notebook.trash_id == None
    assert notebook.read_write == False
    assert notebook.owner == False
    assert notebook.rank == None

    assert result.get( u"login_url" ) is None
    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"

    rate_plan = result[ u"rate_plan" ]
    assert rate_plan[ u"name" ] == u"super"
    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10

  def test_current_after_signup_with_invite_id( self ):
    # trick send_invites() into using a fake SMTP server
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )

    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )

    result = self.http_post( "/users/signup", dict(
      username = self.new_username,
      password = self.new_password,
      password_repeat = self.new_password,
      email_address = self.new_email_address,
      signup_button = u"sign up",
      invite_id = invite_id,
    ) )

    invite_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ]
    assert invite_notebook_id == self.notebooks[ 0 ].object_id

    user = self.database.last_saved_user
    assert isinstance( user, User )
    result = cherrypy.root.users.current( user.object_id )

    assert result[ u"user" ].object_id == user.object_id
    assert result[ u"user" ].username == self.new_username
    assert result[ u"user" ].email_address == self.new_email_address

    assert cherrypy.root.users.check_access( user.object_id, self.notebooks[ 0 ].object_id )
    assert cherrypy.root.users.check_access( user.object_id, self.notebooks[ 0 ].trash_id )

    # the notebook that the user was invited to should be in the list of returned notebooks
    notebooks = dict( [ ( notebook.object_id, notebook ) for notebook in result[ u"notebooks" ] ] )
    
    notebook = notebooks.get( invite_notebook_id )
    assert notebook
    assert notebook.revision
    assert notebook.name == self.notebooks[ 0 ].name
    assert notebook.trash_id
    assert notebook.read_write == False
    assert notebook.owner == False
    assert notebook.rank == 1

    notebook = notebooks.get( self.notebooks[ 0 ].trash_id )
    assert notebook.revision
    assert notebook.name == u"trash"
    assert notebook.trash_id == None
    assert notebook.read_write == False
    assert notebook.owner == False
    assert notebook.rank == None

    notebook = notebooks.get( self.anon_notebook.object_id )
    assert notebook.revision == self.anon_notebook.revision
    assert notebook.name == self.anon_notebook.name
    assert notebook.trash_id == None
    assert notebook.read_write == False
    assert notebook.owner == False
    assert notebook.rank == None

    assert result.get( u"login_url" ) is None
    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"

    rate_plan = result[ u"rate_plan" ]
    assert rate_plan[ u"name" ] == u"super"
    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10

  def test_current_after_signup_with_rate_plan( self ):
    result = self.http_post( "/users/signup", dict(
      username = self.new_username,
      password = self.new_password,
      password_repeat = self.new_password,
      email_address = self.new_email_address,
      signup_button = u"sign up",
      rate_plan = u"2",
    ) )
    session_id = result[ u"session_id" ]

    assert result[ u"redirect" ] == u"/users/subscribe?rate_plan=2"

    user = self.database.last_saved_obj
    assert isinstance( user, User )
    result = cherrypy.root.users.current( user.object_id )

    assert result[ u"user" ].object_id == user.object_id
    assert result[ u"user" ].username == self.new_username
    assert result[ u"user" ].email_address == self.new_email_address

    notebooks = result[ u"notebooks" ]
    notebook = notebooks[ 0 ]
    assert notebook.object_id
    assert notebook.revision
    assert notebook.name == u"my notebook"
    assert notebook.trash_id
    assert notebook.read_write == True
    assert notebook.owner == True
    assert notebook.rank == 0

    notebook = notebooks[ 1 ]
    assert notebook.object_id == notebooks[ 0 ].trash_id
    assert notebook.revision
    assert notebook.name == u"trash"
    assert notebook.trash_id == None
    assert notebook.read_write == True
    assert notebook.owner == True
    assert notebook.rank == None

    notebook = notebooks[ 2 ]
    assert notebook.object_id == self.anon_notebook.object_id
    assert notebook.revision == self.anon_notebook.revision
    assert notebook.name == self.anon_notebook.name
    assert notebook.trash_id == None
    assert notebook.read_write == False
    assert notebook.owner == False
    assert notebook.rank == None

    assert result.get( u"login_url" ) is None
    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"

    rate_plan = result[ u"rate_plan" ]
    assert rate_plan[ u"name" ] == u"super"
    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10

  def test_signup_with_different_passwords( self ):
    result = self.http_post( "/users/signup", dict(
      username = self.new_username,
      password = self.new_password,
      password_repeat = self.new_password + u"nomatch",
      email_address = self.new_email_address,
      signup_button = u"sign up",
    ) )

    assert result[ u"error" ]

  def test_subscribe( self ):
    self.login()

    result = self.http_post( "/users/subscribe", dict(
      rate_plan = u"1",
    ), session_id = self.session_id )

    form = result.get( u"form" )
    plan = self.settings[ u"global" ][ u"luminotes.rate_plans" ][ 1 ]

    assert form == plan[ u"button" ] % self.user.object_id

  def test_subscribe_with_free_rate_plan( self ):
    self.login()

    result = self.http_post( "/users/subscribe", dict(
      rate_plan = u"0",
    ), session_id = self.session_id )

    assert u"plan" in result[ u"error" ]
    assert u"invalid" in result[ u"error" ]

  def test_subscribe_with_invalid_rate_plan( self ):
    self.login()

    result = self.http_post( "/users/subscribe", dict(
      rate_plan = u"17",
    ), session_id = self.session_id )

    assert u"plan" in result[ u"error" ]
    assert u"invalid" in result[ u"error" ]

  def test_subscribe_without_login( self ):
    result = self.http_post( "/users/subscribe", dict(
      rate_plan = u"1",
    ) )

    assert u"user" in result[ u"error" ]
    assert u"invalid" in result[ u"error" ]

  def test_subscribe_without_subscribe_button( self ):
    self.login()
    self.settings[ u"global" ][ u"luminotes.rate_plans" ][ 1 ][ u"button" ] = u"  "

    result = self.http_post( "/users/subscribe", dict(
      rate_plan = u"1",
    ), session_id = self.session_id )


    print result
    assert u"not configured" in result[ u"error" ]

  def test_demo( self ):
    result = self.http_post( "/users/demo", dict() )

    assert result[ u"redirect" ].startswith( u"/notebooks/" )

  def test_current_after_demo( self ):
    result = self.http_post( "/users/demo", dict() )
    session_id = result[ u"session_id" ]

    new_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ]

    user = self.database.last_saved_obj
    assert isinstance( user, User )
    result = cherrypy.root.users.current( user.object_id )

    assert result[ u"user" ].object_id == user.object_id
    assert result[ u"user" ].username is None
    assert result[ u"user" ].email_address is None

    notebooks = result[ u"notebooks" ]
    assert len( notebooks ) == 3
    notebook = notebooks[ 0 ]
    assert notebook.object_id == new_notebook_id
    assert notebook.revision
    assert notebook.name == u"my notebook"
    assert notebook.trash_id
    assert notebook.read_write == True
    assert notebook.owner == True
    assert notebook.rank == 0

    notebook = notebooks[ 1 ]
    assert notebook.object_id == notebooks[ 0 ].trash_id
    assert notebook.revision
    assert notebook.name == u"trash"
    assert notebook.trash_id == None
    assert notebook.read_write == True
    assert notebook.owner == True
    assert notebook.rank == None

    notebook = notebooks[ 2 ]
    assert notebook.object_id == self.anon_notebook.object_id
    assert notebook.revision == self.anon_notebook.revision
    assert notebook.name == self.anon_notebook.name
    assert notebook.trash_id == None
    assert notebook.read_write == False
    assert notebook.owner == False
    assert notebook.rank == None

    assert result.get( u"login_url" ) is None
    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"

    rate_plan = result[ u"rate_plan" ]
    assert rate_plan[ u"name" ] == u"super"
    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10

  def test_current_after_demo_twice( self ):
    result = self.http_post( "/users/demo", dict() )
    session_id = result[ u"session_id" ]

    new_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ]

    user = self.database.last_saved_obj
    assert isinstance( user, User )
    result = cherrypy.root.users.current( user.object_id )

    user_id = result[ u"user" ].object_id
    assert user_id == user.object_id

    # request a demo for a second time
    result = self.http_post( "/users/demo", dict(), session_id = session_id )

    assert result[ u"redirect" ].startswith( u"/notebooks/" )
    notebook_id_again = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ]

    assert notebook_id_again == new_notebook_id

    result = cherrypy.root.users.current( user_id )

    user_id_again = result[ u"user" ].object_id

    # since we're already logged in as a guest user with a demo notebook, requesting a demo again
    # should just use the same guest user with the same notebook
    assert user_id_again == user_id

  def test_login( self ):
    result = self.http_post( "/users/login", dict(
      username = self.username,
      password = self.password,
      login_button = u"login",
    ) )

    assert result[ u"redirect" ] == u"/notebooks/%s" % self.notebooks[ 0 ].object_id

  def test_login_with_unknown_user( self ):
    result = self.http_post( "/users/login", dict(
      username = u"nosuchuser",
      password = self.password,
      login_button = u"login",
    ) )

    assert result[ u"error" ]

  def test_login_with_invalid_password( self ):
    result = self.http_post( "/users/login", dict(
      username = self.username,
      password = u"wrongpass",
      login_button = u"login",
    ) )

    assert result[ u"error" ]

  def test_logout( self ):
    result = self.http_post( "/users/logout", dict() )

    assert result[ u"redirect" ] == self.settings[ u"global" ].get( u"luminotes.http_url" ) + u"/"

  def test_current( self ):
    result = cherrypy.root.users.current( self.user.object_id )

    assert result[ u"user" ]
    assert result[ u"user" ].object_id == self.user.object_id
    assert result[ u"user" ].username == self.user.username
    assert len( result[ u"notebooks" ] ) == 5
    assert result[ u"notebooks" ][ 0 ].object_id == self.notebooks[ 0 ].object_id
    assert result[ u"notebooks" ][ 0 ].name == self.notebooks[ 0 ].name
    assert result[ u"notebooks" ][ 0 ].read_write == True
    assert result[ u"notebooks" ][ 0 ].owner == True
    assert result[ u"notebooks" ][ 0 ].rank == 0
    assert result[ u"notebooks" ][ 1 ].object_id
    assert result[ u"notebooks" ][ 1 ].name == u"trash"
    assert result[ u"notebooks" ][ 1 ].read_write == True
    assert result[ u"notebooks" ][ 1 ].owner == True
    assert result[ u"notebooks" ][ 1 ].rank == None
    assert result[ u"notebooks" ][ 2 ].object_id == self.notebooks[ 1 ].object_id
    assert result[ u"notebooks" ][ 2 ].name == self.notebooks[ 1 ].name
    assert result[ u"notebooks" ][ 2 ].read_write == True
    assert result[ u"notebooks" ][ 2 ].owner == True
    assert result[ u"notebooks" ][ 2 ].rank == 1
    assert result[ u"notebooks" ][ 3 ].object_id
    assert result[ u"notebooks" ][ 3 ].name == u"trash"
    assert result[ u"notebooks" ][ 3 ].read_write == True
    assert result[ u"notebooks" ][ 3 ].owner == True
    assert result[ u"notebooks" ][ 3 ].rank == None
    assert result[ u"notebooks" ][ 4 ].object_id == self.anon_notebook.object_id
    assert result[ u"notebooks" ][ 4 ].name == self.anon_notebook.name
    assert result[ u"notebooks" ][ 4 ].read_write == False
    assert result[ u"notebooks" ][ 4 ].owner == False
    assert result[ u"notebooks" ][ 4 ].rank == None
    assert result[ u"login_url" ] is None
    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"

    rate_plan = result[ u"rate_plan" ]
    assert rate_plan
    assert rate_plan[ u"name" ] == u"super"
    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10

  def test_current_anonymous( self ):
    result = cherrypy.root.users.current( self.anonymous.object_id )

    assert result[ u"user" ].username == "anonymous"
    assert len( result[ u"notebooks" ] ) == 1
    assert result[ u"notebooks" ][ 0 ].object_id == self.anon_notebook.object_id
    assert result[ u"notebooks" ][ 0 ].name == self.anon_notebook.name
    assert result[ u"notebooks" ][ 0 ].read_write == False
    assert result[ u"notebooks" ][ 0 ].owner == False
    assert result[ u"notebooks" ][ 0 ].rank == None

    login_note = self.database.select_one( Note, self.anon_notebook.sql_load_note_by_title( u"login" ) )
    assert result[ u"login_url" ] == u"%s/notebooks/%s?note_id=%s" % (
      self.settings[ u"global" ][ u"luminotes.https_url" ],
      self.anon_notebook.object_id,
      login_note.object_id,
    )
    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"

    rate_plan = result[ u"rate_plan" ]
    assert rate_plan
    assert rate_plan[ u"name" ] == u"super"
    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10

  def test_login_with_invite_id( self ):
    # trick send_invites() into using a fake SMTP server
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )

    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )

    result = self.http_post( "/users/login", dict(
      username = self.username2,
      password = self.password2,
      invite_id = invite_id,
      login_button = u"login",
    ) )

    invite_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ]
    assert invite_notebook_id == self.notebooks[ 0 ].object_id

    assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].object_id )
    assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].trash_id )

  def test_login_with_after_login( self ):
    after_login = u"/foo/bar"

    result = self.http_post( "/users/login", dict(
      username = self.username2,
      password = self.password2,
      after_login = after_login,
      login_button = u"login",
    ) )

    assert result[ u"redirect" ] == after_login

  def test_login_with_after_login_with_full_url( self ):
    after_login = u"http://this_url/does/not/start/with/a/slash"

    result = self.http_post( "/users/login", dict(
      username = self.username2,
      password = self.password2,
      after_login = after_login,
      login_button = u"login",
    ) )

    assert result[ u"redirect" ] == u"/"

  def test_update_storage( self ):
    previous_revision = self.user.revision

    cherrypy.root.users.update_storage( self.user.object_id )

    expected_size = cherrypy.root.users.calculate_storage( self.user )

    user = self.database.load( User, self.user.object_id )
    assert user.storage_bytes == expected_size
    assert user.revision > previous_revision

  def test_update_storage_with_unknown_user_id( self ):
    original_revision = self.user.revision

    cherrypy.root.users.update_storage( 77 )

    expected_size = cherrypy.root.users.calculate_storage( self.user )

    user = self.database.load( User, self.user.object_id )
    assert self.user.storage_bytes == 0
    assert self.user.revision == original_revision

  def test_check_access( self ):
    access = cherrypy.root.users.check_access( self.user.object_id, self.notebooks[ 0 ].object_id )

    assert access is True

  def test_check_access_read_write( self ):
    access = cherrypy.root.users.check_access( self.user.object_id, self.notebooks[ 0 ].object_id, read_write = True )

    assert access is True

  def test_check_access_owner( self ):
    access = cherrypy.root.users.check_access( self.user.object_id, self.notebooks[ 0 ].object_id, owner = True )

    assert access is True

  def test_check_access_full( self ):
    access = cherrypy.root.users.check_access( self.user.object_id, self.notebooks[ 0 ].object_id, read_write = True, owner = True )

    assert access is True

  def test_check_access_anon( self ):
    access = cherrypy.root.users.check_access( self.user.object_id, self.anon_notebook.object_id )

    assert access is True

  def test_check_access_anon_read_write( self ):
    access = cherrypy.root.users.check_access( self.user.object_id, self.anon_notebook.object_id, read_write = True )

    assert access is False

  def test_check_access_anon_owner( self ):
    access = cherrypy.root.users.check_access( self.user.object_id, self.anon_notebook.object_id, owner = True )

    assert access is False

  def test_check_access_anon_full( self ):
    access = cherrypy.root.users.check_access( self.user.object_id, self.anon_notebook.object_id, read_write = True, owner = True )

    assert access is False

  def test_send_reset( self ):
    # trick send_reset() into using a fake SMTP server
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp

    result = self.http_post( "/users/send_reset", dict(
      email_address = self.user.email_address,
      send_reset_button = u"email me",
    ) )
    session_id = result[ u"session_id" ]
    
    assert u"has been sent to" in result[ u"message" ]
    assert smtplib.SMTP.connected == False
    assert "<%s>" % self.settings[ u"global" ][ u"luminotes.support_email" ] in smtplib.SMTP.from_address
    assert smtplib.SMTP.to_addresses == [ self.user.email_address ]
    assert u"password reset" in smtplib.SMTP.message
    assert self.RESET_LINK_PATTERN.search( smtplib.SMTP.message )

  def test_send_reset_to_unknown_email_address( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp

    result = self.http_post( "/users/send_reset", dict(
      email_address = u"unknown@example.com",
      send_reset_button = u"email me",
    ) )
    
    assert u"no Luminotes user" in result[ u"error" ]
    assert smtplib.SMTP.connected == False
    assert smtplib.SMTP.from_address == None
    assert smtplib.SMTP.to_addresses == None
    assert smtplib.SMTP.message == None

  def test_redeem_reset( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp

    self.http_post( "/users/send_reset", dict(
      email_address = self.user.email_address,
      send_reset_button = u"email me",
    ) )

    matches = self.RESET_LINK_PATTERN.search( smtplib.SMTP.message )
    password_reset_id = matches.group( 2 )
    assert password_reset_id

    result = self.http_get( "/users/redeem_reset/%s" % password_reset_id )

    assert result[ u"user" ].username == "anonymous"
    assert len( result[ u"notebooks" ] ) == 1
    assert result[ u"notebooks" ][ 0 ].object_id == self.anon_notebook.object_id
    assert result[ u"notebooks" ][ 0 ].name == self.anon_notebook.name
    assert result[ u"notebooks" ][ 0 ].read_write == False
    assert result[ u"notebooks" ][ 0 ].owner == False
    assert result[ u"notebooks" ][ 0 ].rank == None

    login_note = self.database.select_one( Note, self.anon_notebook.sql_load_note_by_title( u"login" ) )
    assert result[ u"login_url" ] == u"%s/notebooks/%s?note_id=%s" % (
      self.settings[ u"global" ][ u"luminotes.https_url" ],
      self.anon_notebook.object_id,
      login_note.object_id,
    )
    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"

    rate_plan = result[ u"rate_plan" ]
    assert rate_plan
    assert rate_plan[ u"name" ] == u"super"
    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10

    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
    assert len( result[ u"startup_notes" ] ) == 1
    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
    assert result[ u"note_read_write" ] is False

    assert result[ u"notes" ]
    assert len( result[ u"notes" ] ) == 1
    assert result[ u"notes" ][ 0 ].title == u"complete your password reset"
    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
    assert u"password reset" in result[ u"notes" ][ 0 ].contents
    assert self.user.username in result[ u"notes" ][ 0 ].contents
    assert self.user2.username in result[ u"notes" ][ 0 ].contents

  def test_redeem_reset_unknown( self ):
    password_reset_id = u"unknownresetid"
    result = self.http_get( "/users/redeem_reset/%s" % password_reset_id )

    assert u"expired" in result[ u"error" ]

  def test_redeem_reset_expired( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp

    self.http_post( "/users/send_reset", dict(
      email_address = self.user.email_address,
      send_reset_button = u"email me",
    ) )

    matches = self.RESET_LINK_PATTERN.search( smtplib.SMTP.message )
    password_reset_id = matches.group( 2 )
    assert password_reset_id

    # to trigger expiration, pretend that the password reset was made 25 hours ago
    password_reset = self.database.load( Password_reset, password_reset_id )
    password_reset._Persistent__revision = datetime.now( tz = utc ) - timedelta( hours = 25 )
    self.database.save( password_reset )

    result = self.http_get( "/users/redeem_reset/%s" % password_reset_id )

    assert u"expired" in result[ u"error" ]

  def test_redeem_reset_already_redeemed( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp

    self.http_post( "/users/send_reset", dict(
      email_address = self.user.email_address,
      send_reset_button = u"email me",
    ) )

    matches = self.RESET_LINK_PATTERN.search( smtplib.SMTP.message )
    password_reset_id = matches.group( 2 )
    assert password_reset_id

    password_reset = self.database.load( Password_reset, password_reset_id )
    password_reset.redeemed = True
    self.database.save( password_reset )

    result = self.http_get( "/users/redeem_reset/%s" % password_reset_id )

    assert u"already" in result[ u"error" ]

  def test_redeem_reset_unknown_email( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp

    self.http_post( "/users/send_reset", dict(
      email_address = self.user.email_address,
      send_reset_button = u"email me",
    ) )

    matches = self.RESET_LINK_PATTERN.search( smtplib.SMTP.message )
    password_reset_id = matches.group( 2 )
    assert password_reset_id

    password_reset = self.database.load( Password_reset, password_reset_id )
    password_reset._Password_reset__email_address = u"unknown@example.com"
    self.database.save( password_reset )

    result = self.http_get( "/users/redeem_reset/%s" % password_reset_id )

    assert u"email address" in result[ u"error" ]

  def test_reset_password( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp

    self.http_post( "/users/send_reset", dict(
      email_address = self.user.email_address,
      send_reset_button = u"email me",
    ) )

    matches = self.RESET_LINK_PATTERN.search( smtplib.SMTP.message )
    password_reset_id = matches.group( 2 )
    assert password_reset_id

    new_password = u"newpass"
    result = self.http_post( "/users/reset_password", (
      ( u"password_reset_id", password_reset_id ),
      ( u"reset_button", u"reset passwords" ),
      ( self.user.object_id, new_password ),
      ( self.user.object_id, new_password ),
      ( self.user2.object_id, u"" ),
      ( self.user2.object_id, u"" ),
    ) )

    assert result[ u"redirect" ]

    # check that the password reset is now marked as redeemed
    password_reset = self.database.load( Password_reset, password_reset_id )
    assert password_reset.redeemed

    # check that the password was actually reset for one of the users, but not the other
    user = self.database.load( User, self.user.object_id )
    assert user.check_password( new_password )
    user2 = self.database.load( User, self.user2.object_id )
    assert user2.check_password( self.password2 )

  def test_reset_password_unknown_reset_id( self ):
    new_password = u"newpass"
    password_reset_id = u"unknownresetid"
    result = self.http_post( "/users/reset_password", (
      ( u"password_reset_id", password_reset_id ),
      ( u"reset_button", u"reset passwords" ),
      ( self.user.object_id, new_password ),
      ( self.user.object_id, new_password ),
      ( self.user2.object_id, u"" ),
      ( self.user2.object_id, u"" ),
    ) )

    assert u"expired" in result[ "error" ]

    # check that neither user's password has changed
    user = self.database.load( User, self.user.object_id )
    assert user.check_password( self.password )
    user2 = self.database.load( User, self.user2.object_id )
    assert user2.check_password( self.password2 )

  def test_reset_password_invalid_reset_id( self ):
    new_password = u"newpass"
    password_reset_id = u"invalid reset id"
    result = self.http_post( "/users/reset_password", (
      ( u"password_reset_id", password_reset_id ),
      ( u"reset_button", u"reset passwords" ),
      ( self.user.object_id, new_password ),
      ( self.user.object_id, new_password ),
      ( self.user2.object_id, u"" ),
      ( self.user2.object_id, u"" ),
    ) )

    assert u"valid" in result[ "error" ]

    # check that neither user's password has changed
    user = self.database.load( User, self.user.object_id )
    assert user.check_password( self.password )
    user2 = self.database.load( User, self.user2.object_id )
    assert user2.check_password( self.password2 )

  def test_reset_password_expired( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp

    self.http_post( "/users/send_reset", dict(
      email_address = self.user.email_address,
      send_reset_button = u"email me",
    ) )

    matches = self.RESET_LINK_PATTERN.search( smtplib.SMTP.message )
    password_reset_id = matches.group( 2 )
    assert password_reset_id

    # to trigger expiration, pretend that the password reset was made 25 hours ago
    password_reset = self.database.load( Password_reset, password_reset_id )
    password_reset._Persistent__revision = datetime.now( tz = utc ) - timedelta( hours = 25 )
    self.database.save( password_reset )

    new_password = u"newpass"
    result = self.http_post( "/users/reset_password", (
      ( u"password_reset_id", password_reset_id ),
      ( u"reset_button", u"reset passwords" ),
      ( self.user.object_id, new_password ),
      ( self.user.object_id, new_password ),
      ( self.user2.object_id, u"" ),
      ( self.user2.object_id, u"" ),
    ) )

    # check that the password reset is not marked as redeemed
    password_reset = self.database.load( Password_reset, password_reset_id )
    assert password_reset.redeemed == False

    assert u"expired" in result[ "error" ]

    # check that neither user's password has changed
    user = self.database.load( User, self.user.object_id )
    assert user.check_password( self.password )
    user2 = self.database.load( User, self.user2.object_id )
    assert user2.check_password( self.password2 )

  def test_reset_password_non_matching( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp

    self.http_post( "/users/send_reset", dict(
      email_address = self.user.email_address,
      send_reset_button = u"email me",
    ) )

    matches = self.RESET_LINK_PATTERN.search( smtplib.SMTP.message )
    password_reset_id = matches.group( 2 )
    assert password_reset_id

    new_password = u"newpass"
    result = self.http_post( "/users/reset_password", (
      ( u"password_reset_id", password_reset_id ),
      ( u"reset_button", u"reset passwords" ),
      ( self.user.object_id, new_password ),
      ( self.user.object_id, u"nonmatchingpass" ),
      ( self.user2.object_id, u"" ),
      ( self.user2.object_id, u"" ),
    ) )

    assert u"password" in result[ "error" ]

    # check that neither user's password has changed
    user = self.database.load( User, self.user.object_id )
    assert user.check_password( self.password )
    user2 = self.database.load( User, self.user2.object_id )
    assert user2.check_password( self.password2 )

  def test_reset_password_blank( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp

    self.http_post( "/users/send_reset", dict(
      email_address = self.user.email_address,
      send_reset_button = u"email me",
    ) )

    matches = self.RESET_LINK_PATTERN.search( smtplib.SMTP.message )
    password_reset_id = matches.group( 2 )
    assert password_reset_id

    result = self.http_post( "/users/reset_password", (
      ( u"password_reset_id", password_reset_id ),
      ( u"reset_button", u"reset passwords" ),
      ( self.user.object_id, u"" ),
      ( self.user.object_id, u"" ),
      ( self.user2.object_id, u"" ),
      ( self.user2.object_id, u"" ),
    ) )

    assert result[ "error" ]

    # check that neither user's password has changed
    assert self.user.check_password( self.password )
    assert self.user2.check_password( self.password2 )

  def test_reset_password_multiple_users( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp

    self.http_post( "/users/send_reset", dict(
      email_address = self.user.email_address,
      send_reset_button = u"email me",
    ) )

    matches = self.RESET_LINK_PATTERN.search( smtplib.SMTP.message )
    password_reset_id = matches.group( 2 )
    assert password_reset_id

    new_password = u"newpass"
    new_password2 = u"newpass2"
    result = self.http_post( "/users/reset_password", (
      ( u"password_reset_id", password_reset_id ),
      ( u"reset_button", u"reset passwords" ),
      ( self.user.object_id, new_password ),
      ( self.user.object_id, new_password ),
      ( self.user2.object_id, new_password2 ),
      ( self.user2.object_id, new_password2 ),
    ) )

    assert result[ u"redirect" ]

    # check that the password reset is now marked as redeemed
    password_reset = self.database.load( Password_reset, password_reset_id )
    assert password_reset.redeemed

    # check that the password was actually reset for both users
    user = self.database.load( User, self.user.object_id )
    assert user.check_password( new_password )
    user2 = self.database.load( User, self.user2.object_id )
    assert user2.check_password( new_password2 )

  def test_send_invites( self ):
    # trick send_invites() into using a fake SMTP server
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    assert u"An invitation has been sent." in result[ u"message" ]
    invites = result[ u"invites" ]
    assert len( invites ) == 1
    invite = invites[ -1 ]
    assert invite
    assert invite.read_write is False
    assert invite.owner is False

    assert smtplib.SMTP.connected == False
    assert len( smtplib.SMTP.emails ) == 1

    ( from_address, to_addresses, message ) = smtplib.SMTP.emails[ 0 ]
    assert self.email_address in from_address
    assert to_addresses == email_addresses_list
    assert self.notebooks[ 0 ].name in message
    matches = self.INVITE_LINK_PATTERN.search( message )
    invite_id = matches.group( 2 )
    assert invite_id

    # assert that the invite has the read_write / owner flags set appropriately
    invites = self.database.objects.get( invite_id )
    assert invites
    assert len( invites ) == 1
    invite = invites[ -1 ]
    assert invite
    assert invite.read_write is False
    assert invite.owner is False

  def test_send_invites_collaborator( self ):
    # trick send_invites() into using a fake SMTP server
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"collaborator",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    assert u"An invitation has been sent." in result[ u"message" ]
    invites = result[ u"invites" ]
    assert len( invites ) == 1
    invite = invites[ -1 ]
    assert invite
    assert invite.read_write is True
    assert invite.owner is False

    assert smtplib.SMTP.connected == False
    assert len( smtplib.SMTP.emails ) == 1

    ( from_address, to_addresses, message ) = smtplib.SMTP.emails[ 0 ]
    assert self.email_address in from_address
    assert to_addresses == email_addresses_list
    assert self.notebooks[ 0 ].name in message
    matches = self.INVITE_LINK_PATTERN.search( message )
    invite_id = matches.group( 2 )
    assert invite_id

    # assert that the invite has the read_write / owner flags set appropriately
    invites = self.database.objects.get( invite_id )
    assert invites
    assert len( invites ) == 1
    invite = invites[ -1 ]
    assert invite
    assert invite.read_write is True
    assert invite.owner is False

  def test_send_invites_owner( self ):
    # trick send_invites() into using a fake SMTP server
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"owner",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    assert u"An invitation has been sent." in result[ u"message" ]
    invites = result[ u"invites" ]
    assert len( invites ) == 1
    invite = invites[ -1 ]
    assert invite
    assert invite.read_write is True
    assert invite.owner is True

    assert smtplib.SMTP.connected == False
    assert len( smtplib.SMTP.emails ) == 1

    ( from_address, to_addresses, message ) = smtplib.SMTP.emails[ 0 ]
    assert self.email_address in from_address
    assert to_addresses == email_addresses_list
    assert self.notebooks[ 0 ].name in message
    matches = self.INVITE_LINK_PATTERN.search( message )
    invite_id = matches.group( 2 )
    assert invite_id

    # assert that the invite has the read_write / owner flags set appropriately
    invites = self.database.objects.get( invite_id )
    assert invites
    assert len( invites ) == 1
    invite = invites[ -1 ]
    assert invite
    assert invite.read_write is True
    assert invite.owner is True

  def test_send_invites_multiple( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com", u"bar@example.com", u"baz@example.com" ]
    email_addresses = u"Bob <%s>,%s\n %s  " % \
      ( email_addresses_list[ 0 ], email_addresses_list[ 1 ], email_addresses_list[ 2 ] )

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    email_count = len( email_addresses_list )
    invites = result[ u"invites" ]
    assert len( invites ) == email_count
    for invite in invites:
      assert invite
      assert invite.read_write is False
      assert invite.owner is False

    assert u"%s invitations have been sent." % email_count in result[ u"message" ]
    assert smtplib.SMTP.connected == False
    assert len( smtplib.SMTP.emails ) == email_count

    for ( from_address, to_addresses, message ) in smtplib.SMTP.emails:
      assert self.email_address in from_address
      assert len( to_addresses ) == 1
      assert to_addresses[ 0 ] in email_addresses_list
      email_addresses_list.remove( to_addresses[ 0 ] )
      assert self.notebooks[ 0 ].name in message
      assert self.INVITE_LINK_PATTERN.search( message )

  def test_send_invites_duplicate( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com", u"bar@example.com", u"foo@example.com" ]
    email_addresses = u" %s N.E. One <%s>,\n%s" % \
      ( email_addresses_list[ 0 ], email_addresses_list[ 1 ], email_addresses_list[ 2 ] )

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    email_count = len( email_addresses_list ) - 1 # -1 because of the duplicate
    invites = result[ u"invites" ]
    assert len( invites ) == email_count
    for invite in invites:
      assert invite
      assert invite.read_write is False
      assert invite.owner is False
    
    assert u"%s invitations have been sent." % email_count in result[ u"message" ]
    assert smtplib.SMTP.connected == False
    assert len( smtplib.SMTP.emails ) == email_count

    for ( from_address, to_addresses, message ) in smtplib.SMTP.emails:
      assert self.email_address in from_address
      assert len( to_addresses ) == 1
      assert to_addresses[ 0 ] in email_addresses_list
      email_addresses_list.remove( to_addresses[ 0 ] )
      assert self.notebooks[ 0 ].name in message
      assert self.INVITE_LINK_PATTERN.search( message )

  def test_send_invites_similar( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    # first send an invite with read_write and owner set to False
    self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )

    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id1 = matches.group( 2 )
    assert invite_id1

    # login as another user and redeem the invite
    self.login2()
    result = self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id1,
    ), session_id = self.session_id )

    # then send a similar invite to the same email address with read_write and owner set to True
    self.login()
    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"owner",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    
    invites = result[ u"invites" ]
    assert len( invites ) == 1
    invite = invites[ 0 ]
    assert invite
    assert invite.read_write is True
    assert invite.owner is True

    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id2 = matches.group( 2 )
    assert invite_id2

    # assert that both invites have the read_write / owner flags set to True now
    invite1_list = self.database.objects.get( invite_id1 )
    assert invite1_list
    assert len( invite1_list ) >= 2
    invite1 = invite1_list[ -1 ]
    assert invite1
    assert invite1.read_write is True
    assert invite1.owner is True

    invite2_list = self.database.objects.get( invite_id2 )
    assert invite2_list
    assert len( invite2_list ) >= 1
    invite2 = invite2_list[ -1 ]
    assert invite2
    assert invite2.read_write is True
    assert invite2.owner is True

    # assert that the user_notebook table has also been updated accordingly
    access = self.database.select_one( bool, self.user2.sql_has_access(
      self.notebooks[ 0 ].object_id,
      read_write = True,
      owner = True,
    ) )
    assert access is True
    access = self.database.select_one( bool, self.user2.sql_has_access(
      self.notebooks[ 0 ].trash_id,
      read_write = True,
      owner = True,
    ) )
    assert access is True

  def test_send_invites_similar_downgrade( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    # first send an invite with read_write and owner set to False
    self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"collaborator",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    
    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id1 = matches.group( 2 )
    assert invite_id1

    # login as another user and redeem the invite
    self.login2()
    result = self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id1,
    ), session_id = self.session_id )

    # then send a similar invite to the same email address with read_write and owner set to False
    self.login()
    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    
    invites = result[ u"invites" ]
    assert len( invites ) == 1
    invite = invites[ 0 ]
    assert invite
    assert invite.read_write is False
    assert invite.owner is False

    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id2 = matches.group( 2 )
    assert invite_id2

    # assert that both invites have the read_write / owner flags set to False now
    invite1_list = self.database.objects.get( invite_id1 )
    assert invite1_list
    assert len( invite1_list ) >= 2
    invite1 = invite1_list[ -1 ]
    assert invite1
    assert invite1.read_write is False
    assert invite1.owner is False

    invite2_list = self.database.objects.get( invite_id2 )
    assert invite2_list
    assert len( invite2_list ) >= 1
    invite2 = invite2_list[ -1 ]
    assert invite2
    assert invite2.read_write is False
    assert invite2.owner is False

    # assert that the user_notebook table has also been updated accordingly
    access = self.database.select_one( bool, self.user2.sql_has_access(
      self.notebooks[ 0 ].object_id,
      read_write = False,
      owner = False,
    ) )
    assert access is True
    access = self.database.select_one( bool, self.user2.sql_has_access(
      self.notebooks[ 0 ].trash_id,
      read_write = False,
      owner = False,
    ) )
    assert access is True

  def test_send_invites_with_generic_from_address( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    # setting the user's email address to None means the invite will be sent
    # with a "generic" From address (in this case, Luminotes support)
    self.user._User__email_address = None
    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    invites = result[ u"invites" ]
    assert len( invites ) == 1
    invite = invites[ 0 ]
    assert invite
    assert invite.read_write is False
    assert invite.owner is False
    
    assert u"An invitation has been sent." in result[ u"message" ]
    assert smtplib.SMTP.connected == False
    assert len( smtplib.SMTP.emails ) == 1

    ( from_address, to_addresses, message ) = smtplib.SMTP.emails[ 0 ]
    assert self.settings[ u"global" ][ u"luminotes.support_email" ] in from_address
    assert to_addresses == email_addresses_list
    assert self.notebooks[ 0 ].name in message
    assert self.INVITE_LINK_PATTERN.search( message )

  def test_send_invites_without_login( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    assert result[ u"error" ]
    assert "access" in result[ u"error" ]

  def test_send_invites_too_short( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    assert result[ u"error" ]

  def test_send_invites_too_long( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"x" * 5001 ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    assert result[ u"error" ]

  def test_send_invites_no_addresses( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"this is not an @email address!" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    assert result[ u"error" ]

  def test_send_invites_without_username( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user._User__username = None
    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    assert result[ u"error" ]
    assert "access" in result[ u"error" ]

  def test_send_invites_without_any_access( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.database.user_notebook = {}
    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    assert result[ u"error" ]
    assert "access" in result[ u"error" ]

  def test_send_invites_without_read_write_access( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.database.user_notebook = {}
    self.database.execute( self.user.sql_save_notebook( self.notebooks[ 0 ].object_id, read_write = False, owner = True ) )
    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    assert result[ u"error" ]
    assert "access" in result[ u"error" ]

  def test_send_invites_without_owner_access( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.database.user_notebook = {}
    self.database.execute( self.user.sql_save_notebook( self.notebooks[ 0 ].object_id, read_write = True, owner = False ) )
    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    assert result[ u"error" ]
    assert "access" in result[ u"error" ]

  def test_send_invites_viewer_with_lowest_rate_plan( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    invites = result[ u"invites" ]
    assert len( invites ) == 1
    invite = invites[ 0 ]
    assert invite
    assert invite.read_write is False
    assert invite.owner is False
    
    assert u"An invitation has been sent." in result[ u"message" ]
    assert smtplib.SMTP.connected == False
    assert len( smtplib.SMTP.emails ) == 1

    ( from_address, to_addresses, message ) = smtplib.SMTP.emails[ 0 ]
    assert self.email_address in from_address
    assert to_addresses == email_addresses_list
    assert self.notebooks[ 0 ].name in message
    assert self.INVITE_LINK_PATTERN.search( message )

  def test_send_invites_collaborator_with_lowest_rate_plan( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"collaborator",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    assert result[ u"error" ]
    assert "access" in result[ u"error" ]

  def test_send_invites_owner_with_lowest_rate_plan( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"owner",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    assert result[ u"error" ]
    assert "access" in result[ u"error" ]

  def test_send_invites_with_unknown_notebook( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]
    unknown_notebook_id = u"neverheardofit"

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = unknown_notebook_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    session_id = result[ u"session_id" ]
    
    assert result[ u"error" ]
    assert "access" in result[ u"error" ]

  def test_revoke_invite( self ):
    # trick send_invite() into using a fake SMTP server
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    
    assert len( result[ u"invites" ] ) == 1
    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )

    result = self.http_post( "/users/revoke_invite", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      invite_id = invite_id,
    ), session_id = self.session_id )

    assert result[ u"message" ]
    assert len( result[ u"invites" ] ) == 0

  def test_revoke_invite_multiple( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com", u"bar@example.com", u"foo@example.com" ]
    email_addresses = u" ".join( email_addresses_list )

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    
    assert len( result[ u"invites" ] ) == 2
    ( from_address, to_addresses, message ) = smtplib.SMTP.emails[ 0 ]
    matches = self.INVITE_LINK_PATTERN.search( message )
    invite_id = matches.group( 2 )

    result = self.http_post( "/users/revoke_invite", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      invite_id = invite_id,
    ), session_id = self.session_id )

    assert result[ u"message" ]
    assert len( result[ u"invites" ] ) == 1
    assert result[ u"invites" ][ 0 ].email_address == email_addresses_list[ 1 ]

  def test_revoke_invite_redeemed( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    
    assert len( result[ u"invites" ] ) == 1
    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )

    self.login2()
    result = self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id,
    ), session_id = self.session_id )

    assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].object_id )
    assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].trash_id )

    self.login()
    result = self.http_post( "/users/revoke_invite", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      invite_id = invite_id,
    ), session_id = self.session_id )

    assert result[ u"message" ]
    assert len( result[ u"invites" ] ) == 0

    assert not cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].object_id )
    assert not cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].trash_id )

  def test_revoke_invite_redeemed_self( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"owner",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    
    assert len( result[ u"invites" ] ) == 1
    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )

    self.login2()
    result = self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id,
    ), session_id = self.session_id )

    assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].object_id )
    assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].trash_id )

    # as user2, revoke that user's own invite
    result = self.http_post( "/users/revoke_invite", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      invite_id = invite_id,
    ), session_id = self.session_id )

    assert result[ u"message" ]
    assert len( result[ u"invites" ] ) == 0

    # the user should no longer have any access
    assert not cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].object_id )
    assert not cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].trash_id )

  def test_revoke_invite_without_login( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp

    # login to send the invites, but don't send the logged-in session id for revoke_invite() below
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    
    assert len( result[ u"invites" ] ) == 1
    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )

    result = self.http_post( "/users/revoke_invite", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      invite_id = invite_id,
    ) )

    assert result[ u"error" ]
    assert "access" in result[ u"error" ]

  def test_revoke_invite_unknown( self ):
    self.login()

    invite_id = u"unknowninviteid"

    result = self.http_post( "/users/revoke_invite", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      invite_id = invite_id,
    ), session_id = self.session_id )

    assert result[ u"error" ]
    assert "access" in result[ u"error" ]

  def test_revoke_invite_for_incorrect_notebook( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    result = self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    
    assert len( result[ u"invites" ] ) == 1
    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )

    result = self.http_post( "/users/revoke_invite", dict(
      notebook_id = self.notebooks[ 1 ].object_id,
      invite_id = invite_id,
    ), session_id = self.session_id )

    assert result[ u"error" ]
    assert "access" in result[ u"error" ]

  def test_redeem_invite( self ):
    # trick send_invite() into using a fake SMTP server
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )

    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )
    
    result = self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id,
    ) )

    # assert that a redeem invite page is returned with sign up / login links
    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
    assert result[ u"notebook" ].name == self.anon_notebook.name
    assert len( result[ u"startup_notes" ] ) == 1
    assert result[ u"total_notes_count" ] == 1
    assert result[ u"note_read_write" ] == False
    assert len( result[ u"notes" ] ) == 1
    assert result[ u"notes" ][ 0 ].object_id == "redeem_invite"
    assert u"sign up" in result[ u"notes" ][ 0 ].contents
    assert u"login" in result[ u"notes" ][ 0 ].contents
    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
    assert len( result[ u"invites" ] ) == 0

  def test_redeem_invite_unknown( self ):
    result = self.http_post( "/users/redeem_invite", dict(
      invite_id = "unknowninviteid",
    ) )

    assert result[ u"error" ]
    assert u"unknown" in result[ u"error" ]

  def test_redeem_invite_after_login( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )

    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )

    self.login2()
    
    result = self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id,
    ), session_id = self.session_id )

    # assert that access has been granted
    assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].object_id )
    assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].trash_id )

    # assert that the user is redirected to the notebook that the invite is for
    assert result[ u"redirect"].startswith( u"/notebooks/" )
    notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ]
    assert notebook_id == self.notebooks[ 0 ].object_id

  def test_redeem_invite_after_login_already_redeemed( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )

    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )

    self.login2()
    
    self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id,
    ), session_id = self.session_id )

    result = self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id,
    ), session_id = self.session_id )

    # assert that access is still granted
    assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].object_id )
    assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].trash_id )

    # assert that the user is redirected to the notebook that the invite is for
    assert result[ u"redirect"].startswith( u"/notebooks/" )
    notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ]
    assert notebook_id == self.notebooks[ 0 ].object_id

  def test_redeem_invite_after_login_already_redeemed_by_different_user( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )

    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )

    self.login2()
    
    self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id,
    ), session_id = self.session_id )

    self.login()

    result = self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id,
    ), session_id = self.session_id )

    assert u"already" in result[ u"error" ]

  def test_redeem_invite_already_redeemed( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )

    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )
    
    self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id,
    ), session_id = self.session_id )

    result = self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id,
    ) )

    assert result[ u"error" ]
    assert u"already" in result[ u"error" ]

  def test_redeem_invite_already_redeemed( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )

    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )
    
    del( self.database.objects[ self.notebooks[ 0 ].object_id ] )

    result = self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id,
    ) )

    assert result[ u"error" ]
    assert u"unknown" in result[ u"error" ]

  def test_redeem_invite_missing_anonymous_user( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )

    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )
    
    del( self.database.objects[ self.anonymous.object_id ] )

    result = self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id,
    ) )

    assert result[ u"error" ]

  def test_redeem_invite_missing_invite_notebook( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )

    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )
    
    del( self.database.objects[ self.notebooks[ 0 ].object_id ] )

    result = self.http_post( "/users/redeem_invite", dict(
      invite_id = invite_id,
    ) )

    assert result[ u"error" ]

  def test_convert_invite_to_access( self ):
    # start the invitee out with access to one notebook
    self.database.execute( self.user2.sql_save_notebook( self.notebooks[ 1 ].object_id, read_write = True, owner = False, rank = 7 ), commit = False )

    # trick send_invites() into using a fake SMTP server
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    
    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )
    
    # convert the invite to access for a different user
    invite = self.database.load( Invite, invite_id )
    cherrypy.root.users.convert_invite_to_access( invite, self.user2.object_id )

    access = self.database.select_one( bool, self.user2.sql_has_access(
      invite.notebook_id,
      invite.read_write,
      invite.owner,
    ) )
    assert access is True

    notebook = self.database.load( Notebook, invite.notebook_id )
    access = self.database.select_one( bool, self.user2.sql_has_access(
      notebook.trash_id,
      invite.read_write,
      invite.owner,
    ) )
    assert access is True

    self.user.sql_load_notebooks()
    notebooks = self.database.select_many( Notebook, self.user2.sql_load_notebooks() )
    new_notebook = [ notebook for notebook in notebooks if notebook.object_id == invite.notebook_id ][ 0 ]
    print new_notebook.rank
    assert new_notebook.rank == 8 # one higher than the other notebook this user has access to

    assert invite.redeemed_user_id == self.user2.object_id

  def test_convert_invite_to_access_same_user( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    
    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )
    
    # here, the same user that sent the invite is trying to convert it to access
    invite = self.database.load( Invite, invite_id )
    cherrypy.root.users.convert_invite_to_access( invite, self.user.object_id )

    # assert that the user retains the access they already had
    access = self.database.select_one( bool, self.user.sql_has_access(
      invite.notebook_id,
      invite.read_write,
      invite.owner,
    ) )
    assert access is True

    notebook = self.database.load( Notebook, invite.notebook_id )
    access = self.database.select_one( bool, self.user.sql_has_access(
      notebook.trash_id,
      invite.read_write,
      invite.owner,
    ) )
    assert access is True

    # assert that the invite was not actually redeemed
    assert invite.redeemed_user_id == None

  def test_convert_invite_to_access_twice( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    
    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )
    
    invite = self.database.load( Invite, invite_id )
    cherrypy.root.users.convert_invite_to_access( invite, self.user2.object_id )
    cherrypy.root.users.convert_invite_to_access( invite, self.user2.object_id )

    access = self.database.select_one( bool, self.user2.sql_has_access(
      invite.notebook_id,
      invite.read_write,
      invite.owner,
    ) )
    assert access is True

    notebook = self.database.load( Notebook, invite.notebook_id )
    access = self.database.select_one( bool, self.user2.sql_has_access(
      notebook.trash_id,
      invite.read_write,
      invite.owner,
    ) )
    assert access is True

    assert invite.redeemed_user_id == self.user2.object_id

  @raises( Invite_error )
  def test_convert_invite_with_unknown_user( self ):
    Stub_smtp.reset()
    smtplib.SMTP = Stub_smtp
    self.login()

    self.user.rate_plan = 1
    self.database.save( self.user )

    email_addresses_list = [ u"foo@example.com" ]
    email_addresses = email_addresses_list[ 0 ]

    self.http_post( "/users/send_invites", dict(
      notebook_id = self.notebooks[ 0 ].object_id,
      email_addresses = email_addresses,
      access = u"viewer",
      invite_button = u"send invites",
    ), session_id = self.session_id )
    
    matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
    invite_id = matches.group( 2 )
    
    invite = self.database.load( Invite, invite_id )
    cherrypy.root.users.convert_invite_to_access( invite, u"unknown_user_id" )

  PAYMENT_DATA = {
    u"last_name": u"User",
    u"txn_id": u"txn",
    u"receiver_email": u"unittest@luminotes.com",
    u"payment_status": u"Completed",
    u"payment_gross": u"9.00",
    u"residence_country": u"US",
    u"payer_status": u"verified",
    u"txn_type": u"subscr_payment",
    u"payment_date": u"15:38:18 Jan 10 2008 PST",
    u"first_name": u"Test",
    u"item_name": u"Luminotes extra super",
    u"charset": u"windows-1252",
    u"notify_version": u"2.4",
    u"item_number": u"1",
    u"receiver_id": u"rcv",
    u"business": u"unittest@luminotes.com",
    u"payer_id": u"pyr",
    u"verify_sign": u"vfy",
    u"subscr_id": u"sub",
    u"payment_fee": u"0.56",
    u"mc_fee": u"0.56",
    u"mc_currency": u"USD",
    u"payer_email": u"buyer@luminotes.com",
    u"payment_type": u"instant",
    u"mc_gross": u"9.00",
  }

  def test_paypal_notify_payment( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"custom" ] = self.user.object_id
    result = self.http_post( "/users/paypal_notify", data );

    assert len( result ) == 1
    assert result.get( u"session_id" )
    assert Stub_urllib2.result == u"VERIFIED"
    assert Stub_urllib2.headers.get( u"Content-type" ) == u"application/x-www-form-urlencoded"
    assert Stub_urllib2.url.startswith( "https://" )
    assert u"paypal.com" in Stub_urllib2.url
    assert Stub_urllib2.encoded_params

    # being notified of a mere payment should not change the user's rate plan
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_payment_invalid( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"custom" ] = self.user.object_id
    Stub_urllib2.result = u"INVALID"
    try:
      result = self.http_post( "/users/paypal_notify", data );
    finally:
      Stub_urllib2.result = u"VERIFIED"

    assert result.get( u"error" )

  def test_paypal_notify_payment_not_complete( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"payment_status" ] = u"NotEvenRemotelyCompleted"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_payment_incorrect_receiver_email( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"receiver_email" ] = u"someoneelse@luminotes.com"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_payment_incorrect_currency( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_currency" ] = u"EUR"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_payment_invalid_item_number( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"item_number" ] = u"2"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_payment_incorrect_gross( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_gross" ] = u"8.75"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_payment_incorrect_amount( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_amount3" ] = u"1.99"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_payment_incorrect_item_name( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"item_name" ] = u"super professional"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_payment_invalid_period1( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"period1" ] = u"5 Y"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_payment_invalid_period1( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"period2" ] = u"7 M"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_payment_invalid_period3( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"period3" ] = u"2 M"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_payment_missing_custom( self ):
    data = dict( self.PAYMENT_DATA )
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_payment_invalid_custom( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"custom" ] = u"(&^(*&"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_payment_unknown_custom( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"custom" ] = u"1337"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_failed( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"txn_type" ] = u"subscr_failed"
    data[ u"custom" ] = self.user.object_id
    result = self.http_post( "/users/paypal_notify", data );

    assert len( result ) == 1
    assert result.get( u"session_id" )
    assert Stub_urllib2.result == u"VERIFIED"
    assert Stub_urllib2.headers.get( u"Content-type" ) == u"application/x-www-form-urlencoded"
    assert Stub_urllib2.url.startswith( "https://" )
    assert u"paypal.com" in Stub_urllib2.url
    assert Stub_urllib2.encoded_params

    # being notified of a mere failure should not change the user's rate plan
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_failed_invalid( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"txn_type" ] = u"subscr_failed"
    data[ u"custom" ] = self.user.object_id
    Stub_urllib2.result = u"INVALID"
    try:
      result = self.http_post( "/users/paypal_notify", data );
    finally:
      Stub_urllib2.result = u"VERIFIED"

    assert result.get( u"error" )

  def test_paypal_notify_failed_incorrect_receiver_email( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"txn_type" ] = u"subscr_failed"
    data[ u"custom" ] = self.user.object_id
    data[ u"receiver_email" ] = u"someoneelse@luminotes.com"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_failed_incorrect_currency( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"txn_type" ] = u"subscr_failed"
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_currency" ] = u"EUR"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_failed_invalid_item_number( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"txn_type" ] = u"subscr_failed"
    data[ u"custom" ] = self.user.object_id
    data[ u"item_number" ] = u"2"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_failed_incorrect_gross( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"txn_type" ] = u"subscr_failed"
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_gross" ] = u"8.75"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_failed_incorrect_amount( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"txn_type" ] = u"subscr_failed"
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_amount3" ] = u"1.99"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_failed_incorrect_item_name( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"txn_type" ] = u"subscr_failed"
    data[ u"custom" ] = self.user.object_id
    data[ u"item_name" ] = u"super professional"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_failed_invalid_period1( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"txn_type" ] = u"subscr_failed"
    data[ u"custom" ] = self.user.object_id
    data[ u"period1" ] = u"5 Y"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_failed_invalid_period1( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"txn_type" ] = u"subscr_failed"
    data[ u"custom" ] = self.user.object_id
    data[ u"period2" ] = u"7 M"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_failed_invalid_period3( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"txn_type" ] = u"subscr_failed"
    data[ u"custom" ] = self.user.object_id
    data[ u"period3" ] = u"2 M"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_failed_missing_custom( self ):
    data = dict( self.PAYMENT_DATA )
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_failed_invalid_custom( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"txn_type" ] = u"subscr_failed"
    data[ u"custom" ] = u"(&^(*&"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  def test_paypal_notify_failed_unknown_custom( self ):
    data = dict( self.PAYMENT_DATA )
    data[ u"txn_type" ] = u"subscr_failed"
    data[ u"custom" ] = u"1337"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )

  SUBSCRIPTION_DATA = {
    u"last_name": u"User",
    u"receiver_email": u"unittest@luminotes.com",
    u"residence_country": u"US",
    u"payer_status": u"verified",
    u"txn_type": u"subscr_signup",
    u"first_name": u"Test",
    u"item_name": u"Luminotes extra super",
    u"charset": u"windows-1252",
    u"notify_version": u"2.4",
    u"recurring": u"1",
    u"item_number": u"1",
    u"payer_id": u"pyr",
    u"period3": u"1 M",
    u"verify_sign": u"vfy",
    u"subscr_id": u"sub",
    u"amount3": u"9.00",
    u"mc_amount3": u"9.00",
    u"mc_currency": u"USD",
    u"subscr_date": u"15:38:16 Jan 10 2008 PST",
    u"payer_email": u"buyer@luminotes.com",
    u"reattempt": u"1",
  }

  def test_paypal_notify_signup( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"custom" ] = self.user.object_id
    result = self.http_post( "/users/paypal_notify", data );

    assert len( result ) == 1
    assert result.get( u"session_id" )
    assert Stub_urllib2.result == u"VERIFIED"
    assert Stub_urllib2.headers.get( u"Content-type" ) == u"application/x-www-form-urlencoded"
    assert Stub_urllib2.url.startswith( "https://" )
    assert u"paypal.com" in Stub_urllib2.url
    assert Stub_urllib2.encoded_params

    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_signup_invalid( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"custom" ] = self.user.object_id
    Stub_urllib2.result = u"INVALID"
    try:
      result = self.http_post( "/users/paypal_notify", data );
    finally:
      Stub_urllib2.result = u"VERIFIED"

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_signup_incorrect_receiver_email( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"receiver_email" ] = u"someoneelse@luminotes.com"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_signup_incorrect_currency( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_currency" ] = u"EUR"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_signup_invalid_item_number( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"item_number" ] = u"2"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_signup_incorrect_gross( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_gross" ] = u"8.75"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_signup_incorrect_amount( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_amount3" ] = u"1.99"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_signup_incorrect_item_name( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"item_name" ] = u"super professional"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_signup_invalid_period1( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"period1" ] = u"5 Y"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_signup_invalid_period1( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"period2" ] = u"7 M"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_signup_invalid_period3( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"custom" ] = self.user.object_id
    data[ u"period3" ] = u"2 M"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_signup_missing_custom( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_signup_invalid_custom( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"custom" ] = u"(&^(*&"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_signup_unknown_custom( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"custom" ] = u"1337"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_signup_missing_recurring( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    del( data[ u"recurring" ] )
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_signup_invalid_recurring( self ):
    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"recurring" ] = u"0"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_modify( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    data[ u"custom" ] = self.user.object_id
    result = self.http_post( "/users/paypal_notify", data );

    assert len( result ) == 1
    assert result.get( u"session_id" )
    assert Stub_urllib2.result == u"VERIFIED"
    assert Stub_urllib2.headers.get( u"Content-type" ) == u"application/x-www-form-urlencoded"
    assert Stub_urllib2.url.startswith( "https://" )
    assert u"paypal.com" in Stub_urllib2.url
    assert Stub_urllib2.encoded_params

    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_modify_invalid( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    data[ u"custom" ] = self.user.object_id
    Stub_urllib2.result = u"INVALID"
    try:
      result = self.http_post( "/users/paypal_notify", data );
    finally:
      Stub_urllib2.result = u"VERIFIED"

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_modify_incorrect_receiver_email( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    data[ u"custom" ] = self.user.object_id
    data[ u"receiver_email" ] = u"someoneelse@luminotes.com"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_modify_incorrect_currency( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_currency" ] = u"EUR"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_modify_invalid_item_number( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    data[ u"custom" ] = self.user.object_id
    data[ u"item_number" ] = u"2"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_modify_incorrect_gross( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_gross" ] = u"8.75"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_modify_incorrect_amount( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_amount3" ] = u"1.99"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_modify_incorrect_item_name( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    data[ u"custom" ] = self.user.object_id
    data[ u"item_name" ] = u"super professional"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_modify_invalid_period1( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    data[ u"custom" ] = self.user.object_id
    data[ u"period1" ] = u"5 Y"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_modify_invalid_period1( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    data[ u"custom" ] = self.user.object_id
    data[ u"period2" ] = u"7 M"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_modify_invalid_period3( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    data[ u"custom" ] = self.user.object_id
    data[ u"period3" ] = u"2 M"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_modify_missing_custom( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_modify_invalid_custom( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    data[ u"custom" ] = u"(&^(*&"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_modify_unknown_custom( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    data[ u"custom" ] = u"1337"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_modify_missing_recurring( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    del( data[ u"recurring" ] )
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_modify_invalid_recurring( self ):
    self.user.rate_plan = 2
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_modify"
    data[ u"recurring" ] = u"0"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 2

  def test_paypal_notify_cancel( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    data[ u"custom" ] = self.user.object_id
    result = self.http_post( "/users/paypal_notify", data );

    assert len( result ) == 1
    assert result.get( u"session_id" )
    assert Stub_urllib2.result == u"VERIFIED"
    assert Stub_urllib2.headers.get( u"Content-type" ) == u"application/x-www-form-urlencoded"
    assert Stub_urllib2.url.startswith( "https://" )
    assert u"paypal.com" in Stub_urllib2.url
    assert Stub_urllib2.encoded_params

    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 0

  def test_paypal_notify_cancel_invalid( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    data[ u"custom" ] = self.user.object_id
    Stub_urllib2.result = u"INVALID"
    try:
      result = self.http_post( "/users/paypal_notify", data );
    finally:
      Stub_urllib2.result = u"VERIFIED"

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_cancel_incorrect_receiver_email( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    data[ u"custom" ] = self.user.object_id
    data[ u"receiver_email" ] = u"someoneelse@luminotes.com"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_cancel_incorrect_currency( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_currency" ] = u"EUR"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_cancel_invalid_item_number( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    data[ u"custom" ] = self.user.object_id
    data[ u"item_number" ] = u"2"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_cancel_incorrect_gross( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_gross" ] = u"8.75"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_cancel_incorrect_amount( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    data[ u"custom" ] = self.user.object_id
    data[ u"mc_amount3" ] = u"1.99"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_cancel_incorrect_item_name( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    data[ u"custom" ] = self.user.object_id
    data[ u"item_name" ] = u"super professional"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_cancel_invalid_period1( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    data[ u"custom" ] = self.user.object_id
    data[ u"period1" ] = u"5 Y"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_cancel_invalid_period1( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    data[ u"custom" ] = self.user.object_id
    data[ u"period2" ] = u"7 M"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_cancel_invalid_period3( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    data[ u"custom" ] = self.user.object_id
    data[ u"period3" ] = u"2 M"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_cancel_missing_custom( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_cancel_invalid_custom( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    data[ u"custom" ] = u"(&^(*&"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_cancel_unknown_custom( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    data[ u"custom" ] = u"1337"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_cancel_missing_recurring( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    del( data[ u"recurring" ] )
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_paypal_notify_cancel_invalid_recurring( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.SUBSCRIPTION_DATA )
    data[ u"txn_type" ] = u"subscr_cancel"
    data[ u"recurring" ] = u"0"
    result = self.http_post( "/users/paypal_notify", data );

    assert result.get( u"error" )
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  REFUND_DATA = {
    u"last_name": u"User",
    u"txn_id": u"txn",
    u"receiver_email": u"unittest@luminotes.com",
    u"payment_status": u"Refunded",
    u"payment_gross": u"-5.00",
    u"reason_code": u"refund",
    u"residence_country": u"US",
    u"payment_date": u"20:31:40 Jan 11 2008 PST",
    u"first_name": u"Test",
    u"charset": u"windows-1252",
    u"parent_txn_id": u"parent_txn",
    u"notify_version": u"2.4",
    u"item_number": u"1",
    u"receiver_id": u"rcv",
    u"business": u"unittest@luminotes.com",
    u"payer_id": u"pyr",
    u"verify_sign": u"vfy",
    u"subscr_id": u"sub",
    u"payment_fee": u"-0.45",
    u"mc_fee": u"-0.45",
    u"mc_currency": u"USD",
    u"payer_email": u"buyer@luminotes.com",
    u"payment_type": u"instant",
    u"mc_gross": u"-5.00",
  }

  def test_paypal_notify_refund( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    data = dict( self.REFUND_DATA )
    data[ u"custom" ] = self.user.object_id
    result = self.http_post( "/users/paypal_notify", data );

    assert len( result ) == 1
    assert result.get( u"session_id" )

    # being notified of a mere refund should not change the user's rate plan
    user = self.database.load( User, self.user.object_id )
    assert user.rate_plan == 1

  def test_thanks( self ):
    self.user.rate_plan = 1
    user = self.database.save( self.user )

    self.login()

    result = self.http_post( "/users/thanks", dict(
      item_number = u"1",
    ), session_id = self.session_id )

    assert result[ u"user" ].username == self.user.username
    assert len( result[ u"notebooks" ] ) == 5
    assert result[ u"notebooks" ][ 0 ].object_id == self.notebooks[ 0 ].object_id
    assert result[ u"notebooks" ][ 0 ].name == self.notebooks[ 0 ].name
    assert result[ u"notebooks" ][ 0 ].read_write == True
    assert result[ u"notebooks" ][ 0 ].owner == True
    assert result[ u"notebooks" ][ 0 ].rank == 0

    assert result[ u"login_url" ] == None
    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"

    rate_plan = result[ u"rate_plan" ]
    assert rate_plan
    assert rate_plan[ u"name" ] == u"extra super"
    assert rate_plan[ u"storage_quota_bytes" ] == 31337 * 1000

    assert result[ u"conversion" ] == u"subscribe_1"
    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
    assert len( result[ u"startup_notes" ] ) == 1
    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
    assert result[ u"note_read_write" ] is False

    assert result[ u"notes" ]
    assert len( result[ u"notes" ] ) == 1
    assert result[ u"notes" ][ 0 ].title == u"thank you"
    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
    assert u"Thank you" in result[ u"notes" ][ 0 ].contents
    assert u"Luminotes Extra super" in result[ u"notes" ][ 0 ].contents
    assert u"confirmation" not in result[ u"notes" ][ 0 ].contents

  def test_thanks_not_yet_upgraded( self ):
    self.login()

    result = self.http_post( "/users/thanks", dict(
      item_number = u"1",
    ), session_id = self.session_id )

    assert result[ u"user" ].username == self.user.username
    assert result.get( u"conversion" ) == None
    assert len( result[ u"notebooks" ] ) == 5
    assert result[ u"notebooks" ][ 0 ].object_id == self.notebooks[ 0 ].object_id
    assert result[ u"notebooks" ][ 0 ].name == self.notebooks[ 0 ].name
    assert result[ u"notebooks" ][ 0 ].read_write == True
    assert result[ u"notebooks" ][ 0 ].owner == True
    assert result[ u"notebooks" ][ 0 ].rank == 0

    assert result[ u"login_url" ] == None
    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"

    rate_plan = result[ u"rate_plan" ]
    assert rate_plan
    assert rate_plan[ u"name" ] == u"super"
    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10

    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
    assert len( result[ u"startup_notes" ] ) == 1
    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
    assert result[ u"note_read_write" ] is False

    assert result[ u"notes" ]
    assert len( result[ u"notes" ] ) == 1
    assert u"processing" in result[ u"notes" ][ 0 ].title
    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
    assert u"being processed" in result[ u"notes" ][ 0 ].contents
    assert u"retry_count=1" in result[ u"notes" ][ 0 ].contents

  def test_thanks_not_yet_upgraded_with_retry( self ):
    self.login()

    result = self.http_post( "/users/thanks", dict(
      item_number = u"1",
      retry_count = u"5",
    ), session_id = self.session_id )

    assert result[ u"user" ].username == self.user.username
    assert result.get( u"conversion" ) == None
    assert len( result[ u"notebooks" ] ) == 5
    assert result[ u"notebooks" ][ 0 ].object_id == self.notebooks[ 0 ].object_id
    assert result[ u"notebooks" ][ 0 ].name == self.notebooks[ 0 ].name
    assert result[ u"notebooks" ][ 0 ].read_write == True
    assert result[ u"notebooks" ][ 0 ].owner == True
    assert result[ u"notebooks" ][ 0 ].rank == 0

    assert result[ u"login_url" ] == None
    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"

    rate_plan = result[ u"rate_plan" ]
    assert rate_plan
    assert rate_plan[ u"name" ] == u"super"
    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10

    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
    assert len( result[ u"startup_notes" ] ) == 1
    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
    assert result[ u"note_read_write" ] is False

    assert result[ u"notes" ]
    assert len( result[ u"notes" ] ) == 1
    assert u"processing" in result[ u"notes" ][ 0 ].title
    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
    assert u"being processed" in result[ u"notes" ][ 0 ].contents
    assert u"retry_count=6" in result[ u"notes" ][ 0 ].contents

  def test_thanks_with_retry_timeout( self ):
    self.login()

    result = self.http_post( "/users/thanks", dict(
      item_number = u"1",
      retry_count = u"16",
    ), session_id = self.session_id )

    assert result[ u"user" ].username == self.user.username
    assert result.get( u"conversion" ) == None
    assert len( result[ u"notebooks" ] ) == 5
    assert result[ u"notebooks" ][ 0 ].object_id == self.notebooks[ 0 ].object_id
    assert result[ u"notebooks" ][ 0 ].name == self.notebooks[ 0 ].name
    assert result[ u"notebooks" ][ 0 ].read_write == True
    assert result[ u"notebooks" ][ 0 ].owner == True
    assert result[ u"notebooks" ][ 0 ].rank == 0

    assert result[ u"login_url" ] == None
    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"

    rate_plan = result[ u"rate_plan" ]
    assert rate_plan
    assert rate_plan[ u"name" ] == u"super"
    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10

    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
    assert len( result[ u"startup_notes" ] ) == 1
    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
    assert result[ u"note_read_write" ] is False

    assert result[ u"notes" ]
    assert len( result[ u"notes" ] ) == 1
    assert result[ u"notes" ][ 0 ].title == u"thank you"
    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
    assert u"Thank you" in result[ u"notes" ][ 0 ].contents
    assert u"confirmation" in result[ u"notes" ][ 0 ].contents

  def test_thanks_without_item_number( self ):
    self.login()

    result = self.http_post( "/users/thanks", dict(
    ), session_id = self.session_id )

    assert result[ u"user" ].username == self.user.username
    assert result.get( u"conversion" ) == None
    assert len( result[ u"notebooks" ] ) == 5
    assert result[ u"notebooks" ][ 0 ].object_id == self.notebooks[ 0 ].object_id
    assert result[ u"notebooks" ][ 0 ].name == self.notebooks[ 0 ].name
    assert result[ u"notebooks" ][ 0 ].read_write == True
    assert result[ u"notebooks" ][ 0 ].owner == True
    assert result[ u"notebooks" ][ 0 ].rank == 0

    assert result[ u"login_url" ] == None
    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"

    rate_plan = result[ u"rate_plan" ]
    assert rate_plan
    assert rate_plan[ u"name" ] == u"super"
    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10

    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
    assert len( result[ u"startup_notes" ] ) == 1
    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
    assert result[ u"note_read_write" ] is False

    assert result[ u"notes" ]
    assert len( result[ u"notes" ] ) == 1
    assert result[ u"notes" ][ 0 ].title == u"thank you"
    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
    assert u"Thank you" in result[ u"notes" ][ 0 ].contents
    assert u"confirmation" in result[ u"notes" ][ 0 ].contents

  def test_rate_plan( self ):
    plan_index = 1
    rate_plan = cherrypy.root.users.rate_plan( plan_index )

    assert rate_plan
    assert rate_plan == self.settings[ u"global" ][ u"luminotes.rate_plans" ][ plan_index ]

  def login( self ):
    result = self.http_post( "/users/login", dict(
      username = self.username,
      password = self.password,
      login_button = u"login",
    ) )
    self.session_id = result[ u"session_id" ]

  def login2( self ):
    result = self.http_post( "/users/login", dict(
      username = self.username2,
      password = self.password2,
      login_button = u"login",
    ) )
    self.session_id = result[ u"session_id" ]
