1
0
Fork 0

Lots more work on the payment code necessary to support Luminotes Desktop.

master
Dan Helfman 14 years ago
parent 1ae3596f3d
commit 9247683a72
  1. 10
      NEWS
  2. 20
      config/Common.py
  3. 68
      controller/Files.py
  4. 31
      controller/Root.py
  5. 227
      controller/Users.py
  6. 15
      controller/test/Test_controller.py
  7. 142
      controller/test/Test_files.py
  8. 6
      controller/test/Test_root.py
  9. 648
      controller/test/Test_users.py
  10. 75
      model/Download_access.py
  11. 8
      model/delta/1.5.0.sql
  12. 1
      model/drop.sql
  13. 30
      model/schema.sql
  14. 30
      model/schema.sqlite
  15. 15
      model/test/Test_download_access.py
  16. 0
      products/.empty
  17. 16
      view/Download_page.py
  18. 2
      view/Header.py
  19. 26
      view/Processing_download_note.py
  20. 30
      view/Thanks_download_error_note.py
  21. 36
      view/Thanks_download_note.py

10
NEWS

@ -1,5 +1,13 @@
1.5.0 beta 2:
1.5.0:
* Fixed a Luminotes Desktop Internet Explorer bug in which note links within
the "download as html" document pointed to notes in the local Luminotes
installation instead of notes within the stand-alone document.
* Fixed a bug in which Luminotes Desktop file attachment did not always work
due to incorrect upload progress reporting.
* In the revision changes pulldown, no longer showing "by desktopuser" in
Luminotes Desktop.
* Added a Luminotes Desktop download page.
* Added code for supporting product download access.
1.5.0 beta 1: August 27, 2008
* Completed the Luminotes Desktop Windows installer.

@ -107,12 +107,26 @@ settings = {
""",
},
],
"luminotes.download_products": [
{
"name": "Luminotes Desktop",
"designed_for": "individuals",
"storage_quota_bytes": None,
"included_users": 1,
"notebook_sharing": False,
"notebook_collaboration": False,
"user_admin": False,
"fee": "20.00",
"item_number": "5000",
"filename": "luminotes.exe",
"button":
"""
""",
},
],
"luminotes.unsubscribe_button":
"""
""",
"luminotes.download_button":
"""
""",
},
"/files/download": {
"stream_response": True,

@ -18,6 +18,7 @@ from Users import grab_user_id, Access_error
from Expire import strongly_expire
from model.File import File
from model.User import User
from model.Download_access import Download_access
from view.Upload_page import Upload_page
from view.Blank_page import Blank_page
from view.Json import Json
@ -249,7 +250,7 @@ class Files( object ):
"""
Controller for dealing with uploaded files, corresponding to the "/files" URL.
"""
def __init__( self, database, users ):
def __init__( self, database, users, download_products ):
"""
Create a new Files object.
@ -257,11 +258,14 @@ class Files( object ):
@param database: database that file metadata is stored in
@type users: controller.Users
@param users: controller for all users
@type download_products: [ { "name": unicode, ... } ]
@param download_products: list of configured downloadable products
@rtype: Files
@return: newly constructed Files
"""
self.__database = database
self.__users = users
self.__download_products = download_products
@expose()
@end_transaction
@ -331,6 +335,68 @@ class Files( object ):
return stream()
@expose()
@end_transaction
@validate(
access_id = Valid_id(),
item_number = Valid_int(),
)
def download_product( self, access_id, item_number ):
"""
Return the contents of downloadable product file.
@type access_id: unicode
@param access_id: id of download access object that grants access to the file
@type item_number: int or int as unicode
@param item_number: number of the downloadable product
@rtype: generator
@return: file data
@raise Access_error: the access_id is unknown, doesn't grant access to the file, or the
item_number is unknown
"""
# release the session lock before beginning to stream the download. otherwise, if the
# download is cancelled before it's done, the lock won't be released
try:
cherrypy.session.release_lock()
except ( KeyError, OSError ):
pass
# find the product corresponding to the given item_number
products = [
product for product in self.__download_products
if unicode( item_number ) == product.get( u"item_number" )
]
if len( products ) == 0:
raise Access_error()
product = products[ 0 ]
# load the download_access object corresponding to the given id
download_access = self.__database.load( Download_access, access_id )
if download_access is None:
raise Access_error()
public_filename = product[ u"filename" ].encode( "utf8" )
local_filename = u"products/%s" % product[ u"filename" ]
if not os.path.exists( local_filename ):
raise Access_error()
cherrypy.response.headerMap[ u"Content-Type" ] = u"application/octet-stream"
cherrypy.response.headerMap[ u"Content-Disposition" ] = 'attachment; filename="%s"' % public_filename
cherrypy.response.headerMap[ u"Content-Length" ] = os.path.getsize( local_filename )
def stream():
CHUNK_SIZE = 8192
local_file = file( local_filename, "rb" )
while True:
data = local_file.read( CHUNK_SIZE )
if len( data ) == 0: break
yield data
return stream()
@expose( view = File_preview_page )
@end_transaction
@grab_user_id

@ -49,9 +49,14 @@ class Root( object ):
settings[ u"global" ].get( u"luminotes.support_email", u"" ),
settings[ u"global" ].get( u"luminotes.payment_email", u"" ),
settings[ u"global" ].get( u"luminotes.rate_plans", [] ),
settings[ u"global" ].get( u"luminotes.download_products", [] ),
)
self.__groups = Groups( database, self.__users )
self.__files = Files( database, self.__users )
self.__files = Files(
database,
self.__users,
settings[ u"global" ].get( u"luminotes.download_products", [] ),
)
self.__notebooks = Notebooks( database, self.__users, self.__files, settings[ u"global" ].get( u"luminotes.https_url", u"" ) )
self.__forums = Forums( database, self.__users )
self.__suppress_exceptions = suppress_exceptions # used for unit tests
@ -155,6 +160,24 @@ class Root( object ):
redirect = u"/users/redeem_invite/%s" % invite_id,
)
@expose()
def d( self, download_access_id ):
"""
Redirect to the product download thanks URL, based on the given download access id. The sole
purpose of this method is to shorten product download URLs sent by email so email clients don't
wrap them.
"""
# if the value looks like an id, it's a download access id, so redirect
try:
validator = Valid_id()
download_access_id = validator( download_access_id )
except ValueError:
raise cherrypy.NotFound
return dict(
redirect = u"/users/download_thanks/access_id=%s" % download_access_id,
)
@expose( view = Front_page )
@strongly_expire
@end_transaction
@ -350,9 +373,10 @@ class Root( object ):
@end_transaction
@grab_user_id
@validate(
upgrade = Valid_bool( none_okay = True ),
user_id = Valid_id( none_okay = True ),
)
def download( self, user_id = None ):
def download( self, upgrade = False, user_id = None ):
"""
Provide the information necessary to display the Luminotes download page.
"""
@ -363,7 +387,8 @@ class Root( object ):
else:
result[ "first_notebook" ] = None
result[ "download_button" ] = self.__settings[ u"global" ].get( u"luminotes.download_button" )
result[ "download_products" ] = self.__settings[ u"global" ].get( u"luminotes.download_products" )
result[ "upgrade" ] = upgrade
return result

@ -2,6 +2,8 @@ import re
import urllib
import urllib2
import cherrypy
import smtplib
from email import Message
from pytz import utc
from datetime import datetime, timedelta
from model.User import User
@ -9,6 +11,7 @@ from model.Group import Group
from model.Notebook import Notebook
from model.Note import Note
from model.Password_reset import Password_reset
from model.Download_access import Download_access
from model.Invite import Invite
from Expose import expose
from Validate import validate, Valid_string, Valid_bool, Valid_int, Validation_error
@ -21,7 +24,10 @@ from view.Redeem_invite_note import Redeem_invite_note
from view.Blank_page import Blank_page
from view.Thanks_note import Thanks_note
from view.Thanks_error_note import Thanks_error_note
from view.Thanks_download_note import Thanks_download_note
from view.Thanks_download_error_note import Thanks_download_error_note
from view.Processing_note import Processing_note
from view.Processing_download_note import Processing_download_note
from view.Form_submit_page import Form_submit_page
@ -181,7 +187,7 @@ class Users( object ):
"""
Controller for dealing with users, corresponding to the "/users" URL.
"""
def __init__( self, database, http_url, https_url, support_email, payment_email, rate_plans ):
def __init__( self, database, http_url, https_url, support_email, payment_email, rate_plans, download_products ):
"""
Create a new Users object.
@ -195,8 +201,10 @@ class Users( object ):
@param support_email: email address for support requests
@type payment_email: unicode
@param payment_email: email address for payment
@type rate_plans: [ { "name": unicode, "storage_quota_bytes": int } ]
@type rate_plans: [ { "name": unicode, ... } ]
@param rate_plans: list of configured rate plans
@type download_products: [ { "name": unicode, ... } ]
@param download_products: list of configured downloadable products
@rtype: Users
@return: newly constructed Users
"""
@ -206,6 +214,7 @@ class Users( object ):
self.__support_email = support_email
self.__payment_email = payment_email
self.__rate_plans = rate_plans
self.__download_products = download_products
def create_user( self, username, password = None, password_repeat = None, email_address = None, initial_rate_plan = None ):
"""
@ -802,9 +811,6 @@ class Users( object ):
@raise Password_reset_error: an error occured when sending the password reset email
@raise Validation_error: one of the arguments is invalid
"""
import smtplib
from email import Message
# check whether there are actually any users with the given email address
users = self.__database.select_many( User, User.sql_load_by_email_address( email_address ) )
@ -1252,6 +1258,9 @@ class Users( object ):
self.__database.commit()
#PAYPAL_URL = u"https://www.sandbox.paypal.com/cgi-bin/webscr"
PAYPAL_URL = u"https://www.paypal.com/cgi-bin/webscr"
@expose( view = Blank_page )
@end_transaction
def paypal_notify( self, **params ):
@ -1262,9 +1271,6 @@ class Users( object ):
record in the database with their new rate plan. paypal_notify() is
invoked by PayPal itself.
"""
#PAYPAL_URL = u"https://www.sandbox.paypal.com/cgi-bin/webscr"
PAYPAL_URL = u"https://www.paypal.com/cgi-bin/webscr"
# check that payment_status is Completed
payment_status = params.get( u"payment_status" )
if payment_status == u"Refunded":
@ -1283,19 +1289,117 @@ class Users( object ):
raise Payment_error( u"unsupported mc_currency", params )
# verify item_number
plan_index = params.get( u"item_number" )
if plan_index == None or plan_index == u"":
item_number = params.get( u"item_number" )
if item_number == None or item_number == u"":
return dict() # ignore this transaction if there's no item number
try:
plan_index = int( plan_index )
int( item_number )
except ValueError:
raise Payment_error( u"invalid item_number", params )
if plan_index == 0 or plan_index >= len( self.__rate_plans ):
raise Payment_error( u"invalid item_number", params )
product = None
for potential_product in self.__download_products:
if unicode( item_number ) == potential_product.get( u"item_number" ):
product = potential_product
if product:
self.__paypal_notify_download( params, product, unicode( item_number ) )
else:
plan_index = int( item_number )
try:
rate_plan = self.__rate_plans[ plan_index ]
except IndexError:
raise Payment_error( u"invalid item_number", params )
self.__paypal_notify_subscribe( params, rate_plan, plan_index )
return dict()
TRANSACTION_ID_PATTERN = re.compile( u"^[a-zA-Z0-9]+$" )
def __paypal_notify_download( self, params, product, item_number ):
# verify that quantity * the expected fee == mc_gross
fee = float( product[ u"fee" ] )
try:
mc_gross = float( params.get( u"mc_gross" ) )
if not mc_gross: raise ValueError()
except ( TypeError, ValueError ):
raise Payment_error( u"invalid mc_gross", params )
try:
quantity = float( params.get( u"quantity" ) )
if not quantity: raise ValueError()
except ( TypeError, ValueError ):
raise Payment_error( u"invalid quantity", params )
if quantity * fee != mc_gross:
raise Payment_error( u"invalid mc_gross", params )
# verify item_name
item_name = params.get( u"item_name" )
if item_name and product[ u"name" ].lower() not in item_name.lower():
raise Payment_error( u"invalid item_name", params )
params[ u"cmd" ] = u"_notify-validate"
encoded_params = urllib.urlencode( params )
# verify txn_type
txn_type = params.get( u"txn_type" )
if txn_type and txn_type != u"web_accept":
raise Payment_error( u"invalid txn_type", params )
# verify txn_id
txn_id = params.get( u"txn_id" )
if not self.TRANSACTION_ID_PATTERN.search( txn_id ):
raise Payment_error( u"invalid txn_id", params )
# ask paypal to verify the request
request = urllib2.Request( self.PAYPAL_URL )
request.add_header( u"Content-type", u"application/x-www-form-urlencoded" )
request_file = urllib2.urlopen( self.PAYPAL_URL, encoded_params )
result = request_file.read()
if result != u"VERIFIED":
raise Payment_error( result, params )
# update the database with a record of the transaction, thereby giving the user access to the
# download
download_access_id = self.__database.next_id( Download_access, commit = False )
download_access = Download_access.create( download_access_id, item_number, txn_id )
self.__database.save( download_access, commit = False )
self.__database.commit()
# using the reported payer email, send the user an email with a download link
email_address = params.get( u"payer_email" )
if not email_address:
return
# create an email message with a unique invitation link
message = Message.Message()
message[ u"From" ] = u"Luminotes personal wiki <%s>" % self.__support_email
message[ u"To" ] = email_address
message[ u"Subject" ] = u"Luminotes Desktop download"
payload = \
u"Thank you for purchasing Luminotes Desktop!\n\n" + \
u"To download the installer, please follow this link:\n\n" + \
u"%s/d/%s\n\n" % ( self.__https_url or self.__http_url, download_access_id ) + \
u"You can use this link anytime to download Luminotes Desktop or upgrade\n" + \
u"to new versions as they are released. So you should probably keep the" + \
u"link around.\n\n" + \
u"If you have any questions, please email support@luminotes.com\n\n" + \
u"Enjoy!"
message.set_payload( payload )
# send the message out through localhost's smtp server
server = smtplib.SMTP()
server.connect()
server.sendmail( message[ u"From" ], [ email_address ], message.as_string() )
server.quit()
def __paypal_notify_subscribe( self, params, rate_plan, plan_index ):
# verify mc_gross
rate_plan = self.__rate_plans[ plan_index ]
fee = u"%0.2f" % rate_plan[ u"fee" ]
yearly_fee = u"%0.2f" % rate_plan[ u"yearly_fee" ]
mc_gross = params.get( u"mc_gross" )
@ -1329,9 +1433,9 @@ class Users( object ):
encoded_params = urllib.urlencode( params )
# ask paypal to verify the request
request = urllib2.Request( PAYPAL_URL )
request = urllib2.Request( self.PAYPAL_URL )
request.add_header( u"Content-type", u"application/x-www-form-urlencoded" )
request_file = urllib2.urlopen( PAYPAL_URL, encoded_params )
request_file = urllib2.urlopen( self.PAYPAL_URL, encoded_params )
result = request_file.read()
if result != u"VERIFIED":
@ -1366,8 +1470,6 @@ class Users( object ):
else:
raise Payment_error( "unknown txn_type", params )
return dict()
def update_groups( self, user ):
"""
Update a user's group membership as a result of a rate plan change. This method does not commit
@ -1468,6 +1570,93 @@ class Users( object ):
def rate_plan( self, plan_index ):
return self.__rate_plans[ plan_index ]
@expose( view = Main_page )
@end_transaction
@grab_user_id
def thanks_download( self, **params ):
"""
Provide the information necessary to display the download thanks page, including a product
download link. This information can be accessed with an item_number and either a txn_id or a
download access_id.
"""
item_number = params.get( u"item_number" )
try:
item_number = int( item_number )
except ( TypeError, ValueError ):
raise Payment_error( u"invalid item_number", params )
# if a valid txn_id is provided, redirect to this page with the corresponding access_id.
# that way, if the user bookmarks the page, they'll bookmark it with the access_id rather
# than the txn_id
txn_id = params.get( u"txn_id" )
if txn_id:
if not self.TRANSACTION_ID_PATTERN.search( txn_id ):
raise Payment_error( u"invalid txn_id", params )
download_access = self.__database.select_one( Download_access, Download_access.sql_load_by_transaction_id( txn_id ) )
if download_access:
return dict(
redirect = u"/users/thanks_download?access_id=%s&item_number=%s" % ( download_access.object_id, item_number )
)
download_access_id = params.get( u"access_id" )
download_url = None
if download_access_id:
try:
Valid_id()( download_access_id )
except ValueError:
raise Payment_error( u"invalid access_id", params )
download_access = self.__database.load( Download_access, download_access_id )
if download_access:
if download_access.item_number != unicode( item_number ):
raise Payment_error( u"incorrect item_number", params )
download_url = u"%s/files/download_product/access_id=%s&item_number=%s" % \
( self.__https_url or u"", download_access_id, item_number )
if not txn_id and not download_access_id:
raise Payment_error( u"either txn_id or access_id required", params )
anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ), use_cache = True )
if anonymous:
main_notebook = self.__database.select_one( Notebook, anonymous.sql_load_notebooks( undeleted_only = True ) )
else:
main_notebook = None
result = self.current( params.get( u"user_id" ) )
retry_count = params.get( u"retry_count", "" )
try:
retry_count = int( retry_count )
except ValueError:
retry_count = None
# if there's no download access or we've retried too many times, give up and display an error
RETRY_TIMEOUT = 15
if download_url is None and retry_count > RETRY_TIMEOUT:
note = Thanks_download_error_note()
# if the rate plan of the subscription matches the user's current rate plan, success
elif download_url:
note = Thanks_download_note( download_url )
result[ "conversion" ] = "download_%s" % item_number
# otherwise, display an auto-reloading "processing..." page
else:
note = Processing_download_note( download_access_id, item_number, retry_count )
result[ "notebook" ] = main_notebook
result[ "startup_notes" ] = self.__database.select_many( Note, main_notebook.sql_load_startup_notes() )
result[ "total_notes_count" ] = self.__database.select_one( Note, main_notebook.sql_count_notes(), use_cache = True )
result[ "note_read_write" ] = False
result[ "notes" ] = [ Note.create(
object_id = u"thanks",
contents = unicode( note ),
notebook_id = main_notebook.object_id,
) ]
result[ "invites" ] = []
return result
@expose( view = Json )
@end_transaction
@grab_user_id

@ -96,6 +96,21 @@ class Test_controller( object ):
u"yearly_button": u"[yearly or here user %s!] button",
},
],
"luminotes.download_products": [
{
"name": "local desktop extravaganza",
"designed_for": "individuals",
"storage_quota_bytes": None,
"included_users": 1,
"notebook_sharing": False,
"notebook_collaboration": False,
"user_admin": False,
"fee": "30.00",
"item_number": "5000",
"filename": "test.exe",
"button": u"",
},
],
},
u"/files/download": {
u"stream_response": True,

@ -1,5 +1,6 @@
# -*- coding: utf8 -*-
import os
import time
import types
import urllib
@ -14,6 +15,7 @@ from model.Note import Note
from model.User import User
from model.Invite import Invite
from model.File import File
from model.Download_access import Download_access
from controller.Notebooks import Access_error
from controller.Files import Upload_file, Parse_error
@ -90,6 +92,11 @@ class Test_files( Test_controller ):
Upload_file.exists = exists
Upload_file.close = close
# write a test product file
test_product_file = file( u"products/test.exe", "wb" )
test_product_file.write( self.file_data )
test_product_file.close()
self.make_users()
self.make_notebooks()
self.database.commit()
@ -128,6 +135,8 @@ class Test_files( Test_controller ):
if self.upload_thread:
self.upload_thread.join()
os.remove( u"products/test.exe" )
def test_download( self, filename = None, quote_filename = None, file_data = None, preview = None ):
self.login()
@ -327,6 +336,139 @@ class Test_files( Test_controller ):
assert u"access" in result[ u"body" ][ 0 ]
def test_download_product( self ):
access_id = u"wheeaccessid"
item_number = u"5000"
transaction_id = u"txn"
self.login()
download_access = Download_access.create( access_id, item_number, transaction_id )
self.database.save( download_access )
result = self.http_get(
"/files/download_product?access_id=%s&item_number=%s" % ( access_id, item_number ),
session_id = self.session_id,
)
headers = result[ u"headers" ]
assert headers
assert headers[ u"Content-Type" ] == u"application/octet-stream"
filename = u"test.exe".encode( "utf8" )
assert headers[ u"Content-Disposition" ] == 'attachment; filename="%s"' % filename
gen = result[ u"body" ]
assert isinstance( gen, types.GeneratorType )
pieces = []
try:
for piece in gen:
pieces.append( piece )
except AttributeError, exc:
if u"session_storage" not in str( exc ):
raise exc
file_data = "".join( pieces )
assert file_data == self.file_data
def test_download_product_without_login( self ):
access_id = u"wheeaccessid"
item_number = u"5000"
transaction_id = u"txn"
download_access = Download_access.create( access_id, item_number, transaction_id )
self.database.save( download_access )
result = self.http_get(
"/files/download_product?access_id=%s&item_number=%s" % ( access_id, item_number ),
)
headers = result[ u"headers" ]
assert headers
assert headers[ u"Content-Type" ] == u"application/octet-stream"
filename = u"test.exe".encode( "utf8" )
assert headers[ u"Content-Disposition" ] == 'attachment; filename="%s"' % filename
gen = result[ u"body" ]
assert isinstance( gen, types.GeneratorType )
pieces = []
try:
for piece in gen:
pieces.append( piece )
except AttributeError, exc:
if u"session_storage" not in str( exc ):
raise exc
file_data = "".join( pieces )
assert file_data == self.file_data
def test_download_product_unknown_access_id( self ):
access_id = u"wheeaccessid"
item_number = u"5000"
transaction_id = u"txn"
self.login()
download_access = Download_access.create( access_id, item_number, transaction_id )
self.database.save( download_access )
result = self.http_get(
"/files/download_product?access_id=%s&item_number=%s" % ( u"unknownid", item_number ),
session_id = self.session_id,
)
assert u"access" in result[ u"body" ][ 0 ]
headers = result[ u"headers" ]
assert headers
assert headers[ u"Content-Type" ] == u"text/html"
assert not headers.get( u"Content-Disposition" )
def test_download_product_unknown_item_number( self ):
access_id = u"wheeaccessid"
item_number = u"5000"
transaction_id = u"txn"
self.login()
download_access = Download_access.create( access_id, item_number, transaction_id )
self.database.save( download_access )
result = self.http_get(
"/files/download_product?access_id=%s&item_number=%s" % ( access_id, u"1137" ),
session_id = self.session_id,
)
assert u"access" in result[ u"body" ][ 0 ]
headers = result[ u"headers" ]
assert headers
assert headers[ u"Content-Type" ] == u"text/html"
assert not headers.get( u"Content-Disposition" )
def test_download_product_missing_file( self ):
access_id = u"wheeaccessid"
item_number = u"5000"
transaction_id = u"txn"
self.settings[ u"global" ][ u"luminotes.download_products" ][ 0 ][ u"filename" ] = u"notthere.exe"
self.login()
download_access = Download_access.create( access_id, item_number, transaction_id )
self.database.save( download_access )
result = self.http_get(
"/files/download_product?access_id=%s&item_number=%s" % ( access_id, item_number ),
session_id = self.session_id,
)
assert u"access" in result[ u"body" ][ 0 ]
headers = result[ u"headers" ]
assert headers
assert headers[ u"Content-Type" ] == u"text/html"
assert not headers.get( u"Content-Disposition" )
def test_preview( self ):
self.login()

@ -538,3 +538,9 @@ class Test_root( Test_controller ):
result = self.http_get( "/i/%s" % invite_id )
assert result[ u"redirect" ] == u"/users/redeem_invite/%s" % invite_id
def test_download_thanks( self ):
download_access_id = u"foobarbaz"
result = self.http_get( "/d/%s" % download_access_id )
assert result[ u"redirect" ] == u"/users/download_thanks/access_id=%s" % download_access_id

@ -7,11 +7,13 @@ from nose.tools import raises
from datetime import datetime, timedelta
from Test_controller import Test_controller
import Stub_urllib2
from config.Version import VERSION
from model.User import User
from model.Group import Group
from model.Notebook import Notebook
from model.Note import Note
from model.Password_reset import Password_reset
from model.Download_access import Download_access
from model.Invite import Invite
from controller.Users import Invite_error, Payment_error
import controller.Users as Users
@ -3944,6 +3946,150 @@ class Test_users( Test_controller ):
user = self.database.load( User, self.user.object_id )
assert user.rate_plan == 1
DOWNLOAD_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"30.00",
u"residence_country": u"US",
u"payer_status": u"verified",
u"txn_type": u"web_accept",
u"payment_date": u"15:38:18 Jan 10 2008 PST",
u"first_name": u"Test",
u"item_name": u"local desktop extravaganza",
u"charset": u"windows-1252",
u"notify_version": u"2.4",
u"item_number": u"5000",
u"receiver_id": u"rcv",
u"business": u"unittest@luminotes.com",
u"payer_id": u"pyr",
u"verify_sign": u"vfy",
u"payment_fee": u"1.19",
u"mc_fee": u"1.19",
u"mc_currency": u"USD",
u"shipping": u"0.00",
u"payer_email": u"buyer@luminotes.com",
u"payment_type": u"instant",
u"mc_gross": u"30.00",
u"quantity": u"1",
}
def __assert_download_payment_success( self, result, expect_email = True ):
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
# verify that the user has been granted download access
download_access = self.database.select_one( Download_access, "select * from download_access order by revision desc limit 1;" );
assert download_access
assert download_access.item_number == u"5000"
assert download_access.transaction_id == u"txn"
if not expect_email:
return
# verify that an email has been sent to the user
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 == [ u"buyer@luminotes.com" ]
assert u"Thank you" in smtplib.SMTP.message
assert u"download" in smtplib.SMTP.message
assert u"upgrade" in smtplib.SMTP.message
expected_download_link = u"%s/d/%s" % \
( self.settings[ u"global" ][ u"luminotes.https_url" ], download_access.object_id )
assert expected_download_link in smtplib.SMTP.message
def test_paypal_notify_download_payment( self ):
data = dict( self.DOWNLOAD_PAYMENT_DATA )
result = self.http_post( "/users/paypal_notify", data );
self.__assert_download_payment_success( result )
def test_paypal_notify_download_payment_multiple_quantity( self ):
data = dict( self.DOWNLOAD_PAYMENT_DATA )
data[ u"mc_gross" ] = u"90.0"
data[ u"quantity" ] = u"3"
result = self.http_post( "/users/paypal_notify", data );
self.__assert_download_payment_success( result )
def __assert_download_payment_error( self, result ):
assert u"error" in result
download_access = self.database.select_one( Download_access, "select * from download_access order by revision desc limit 1;" );
assert not download_access
assert not smtplib.SMTP.message
def test_paypal_notify_download_payment_missing_mc_gross( self ):
data = dict( self.DOWNLOAD_PAYMENT_DATA )
del( data[ u"mc_gross" ] )
result = self.http_post( "/users/paypal_notify", data );
self.__assert_download_payment_error( result )
def test_paypal_notify_download_payment_none_mc_gross( self ):
data = dict( self.DOWNLOAD_PAYMENT_DATA )
data[ u"mc_gross" ] = None
result = self.http_post( "/users/paypal_notify", data );
self.__assert_download_payment_error( result )
def test_paypal_notify_download_payment_missing_quantity( self ):
data = dict( self.DOWNLOAD_PAYMENT_DATA )
del( data[ u"quantity" ] )
result = self.http_post( "/users/paypal_notify", data );
self.__assert_download_payment_error( result )
def test_paypal_notify_download_payment_none_quantity( self ):
data = dict( self.DOWNLOAD_PAYMENT_DATA )
data[ u"quantity" ] = None
result = self.http_post( "/users/paypal_notify", data );
self.__assert_download_payment_error( result )
def test_paypal_notify_download_payment_quantity_mc_gross_mismatch( self ):
data = dict( self.DOWNLOAD_PAYMENT_DATA )
data[ u"quantity" ] = u"2"
result = self.http_post( "/users/paypal_notify", data );
self.__assert_download_payment_error( result )
def test_paypal_notify_download_payment_mc_gross_fee_mismatch( self ):
data = dict( self.DOWNLOAD_PAYMENT_DATA )
data[ u"quantity" ] = u"2"
data[ u"mc_gross" ] = u"61.0"
result = self.http_post( "/users/paypal_notify", data );
self.__assert_download_payment_error( result )
def test_paypal_notify_download_payment_invalid_item_name( self ):
data = dict( self.DOWNLOAD_PAYMENT_DATA )
data[ u"item_name" ] = u"something unexpected"
result = self.http_post( "/users/paypal_notify", data );
self.__assert_download_payment_error( result )
def test_paypal_notify_download_payment_partial_item_name( self ):
data = dict( self.DOWNLOAD_PAYMENT_DATA )
data[ u"item_name" ] = u"ultra LOCAL DESKTOP extravaganza digital download!"
result = self.http_post( "/users/paypal_notify", data );
self.__assert_download_payment_success( result )
def test_paypal_notify_download_payment_invalid_txn_type( self ):
data = dict( self.DOWNLOAD_PAYMENT_DATA )
data[ u"txn_type" ] = u"web_wtf"
result = self.http_post( "/users/paypal_notify", data );
self.__assert_download_payment_error( result )
def test_paypal_notify_download_payment_invalid_txn_id( self ):
data = dict( self.DOWNLOAD_PAYMENT_DATA )
data[ u"txn_id" ] = u"not even remotely valid"
result = self.http_post( "/users/paypal_notify", data );
self.__assert_download_payment_error( result )
def test_paypal_notify_download_payment_missing_payer_email( self ):
data = dict( self.DOWNLOAD_PAYMENT_DATA )
data[ u"payer_email" ] = u""
result = self.http_post( "/users/paypal_notify", data );
self.__assert_download_payment_success( result, expect_email = False )
def test_thanks( self ):
self.user.rate_plan = 1
user = self.database.save( self.user )
@ -4143,6 +4289,508 @@ class Test_users( Test_controller ):
assert u"Thank you" in result[ u"notes" ][ 0 ].contents
assert u"confirmation" in result[ u"notes" ][ 0 ].contents
def test_thanks_download( self ):
access_id = u"wheeaccessid"
item_number = u"5000"
transaction_id = u"txn"
download_access = Download_access.create( access_id, item_number, transaction_id )
self.database.save( download_access )
self.login()
result = self.http_post( "/users/thanks_download", dict(
access_id = access_id,
item_number = item_number,
), session_id = self.session_id )
assert result[ u"user" ].username == self.user.username
assert len( result[ u"notebooks" ] ) == 5
notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
assert notebook.object_id == self.notebooks[ 0 ].object_id
assert notebook.name == self.notebooks[ 0 ].name
assert notebook.read_write == True
assert notebook.owner == True
assert notebook.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"conversion" ] == u"download_5000"
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 Desktop" in result[ u"notes" ][ 0 ].contents
assert u"Download" in result[ u"notes" ][ 0 ].contents
assert VERSION in result[ u"notes" ][ 0 ].contents
expected_download_link = u"%s/files/download_product/access_id=%s&item_number=%s" % \
( self.settings[ u"global" ][ u"luminotes.https_url" ], access_id, item_number )
assert expected_download_link in result[ u"notes" ][ 0 ].contents
def test_thanks_download_without_login( self ):
access_id = u"wheeaccessid"
item_number = u"5000"
transaction_id = u"txn"
download_access = Download_access.create( access_id, item_number, transaction_id )
self.database.save( download_access )
result = self.http_post( "/users/thanks_download", dict(
access_id = access_id,
item_number = item_number,
) )
assert result[ u"user" ].username == self.anonymous.username
assert len( result[ u"notebooks" ] ) == 1
assert result[ u"login_url" ]
assert result[ u"logout_url" ]
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"conversion" ] == u"download_5000"
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 Desktop" in result[ u"notes" ][ 0 ].contents
assert u"Download" in result[ u"notes" ][ 0 ].contents
assert VERSION in result[ u"notes" ][ 0 ].contents
expected_download_link = u"%s/files/download_product/access_id=%s&item_number=%s" % \
( self.settings[ u"global" ][ u"luminotes.https_url" ], access_id, item_number )
assert expected_download_link in result[ u"notes" ][ 0 ].contents
def test_thanks_download_invalid_item_number( self ):
access_id = u"wheeaccessid"
item_number = u"5000abc"
transaction_id = u"txn"
download_access = Download_access.create( access_id, item_number, transaction_id )
self.database.save( download_access )
self.login()
result = self.http_post( "/users/thanks_download", dict(
access_id = access_id,
item_number = item_number,
), session_id = self.session_id )
assert u"error" in result
def test_thanks_download_none_item_number( self ):
access_id = u"wheeaccessid"
item_number = None
transaction_id = u"txn"
download_access = Download_access.create( access_id, item_number, transaction_id )
self.database.save( download_access )
self.login()
result = self.http_post( "/users/thanks_download", dict(
access_id = access_id,
item_number = item_number,
), session_id = self.session_id )
assert u"error" in result
def test_thanks_download_missing_item_number( self ):
access_id = u"wheeaccessid"
transaction_id = u"txn"
self.login()
result = self.http_post( "/users/thanks_download", dict(
access_id = access_id,
), session_id = self.session_id )
assert u"error" in result
def test_thanks_download_incorrect_item_number( self ):
access_id = u"wheeaccessid"
item_number = u"5000"
transaction_id = u"txn"
self.login()
download_access = Download_access.create( access_id, item_number, transaction_id )
self.database.save( download_access )
result = self.http_post( "/users/thanks_download", dict(
access_id = access_id,
item_number = u"1234",
), session_id = self.session_id )
assert u"error" in result
def test_thanks_download_txn_id( self ):
access_id = u"wheeaccessid"
item_number = u"5000"
transaction_id = u"txn"
download_access = Download_access.create( access_id, item_number, transaction_id )
self.database.save( download_access )
self.login()
result = self.http_post( "/users/thanks_download", dict(
txn_id = transaction_id,
item_number = item_number,
), session_id = self.session_id )
redirect = result.get( u"redirect" )
expected_redirect = "/users/thanks_download?access_id=%s&item_number=%s" % ( access_id, item_number )
assert redirect == expected_redirect
def test_thanks_download_invalid_txn_id( self ):
access_id = u"wheeaccessid"
item_number = u"5000"
transaction_id = u"invalid txn id"
download_access = Download_access.create( access_id, item_number, transaction_id )
self.database.save( download_access )
self.login()
result = self.http_post( "/users/thanks_download", dict(
txn_id = transaction_id,
item_number = item_number,
), session_id = self.session_id )
assert u"error" in result
def test_thanks_download_not_yet_paid( self ):
access_id = u"wheeaccessid"
item_number = u"5000"
transaction_id = u"txn"
self.login()
result = self.http_post( "/users/thanks_download", dict(
access_id = access_id,
item_number = item_number,
), session_id = self.session_id )
# an unknown transaction id might just mean we're still waiting for the transaction to come in,
# so expect a retry
assert result[ u"user" ].username == self.user.username
assert len( result[ u"notebooks" ] ) == 5
notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
assert notebook.object_id == self.notebooks[ 0 ].object_id
assert notebook.name == self.notebooks[ 0 ].name
assert notebook.read_write == True
assert notebook.owner == True
assert notebook.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 not result.get( u"conversion" )
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_download_not_yet_paid_with_retry( self ):
access_id = u"wheeaccessid"
item_number = u"5000"
transaction_id = u"txn"
self.login()
result = self.http_post( "/users/thanks_download", dict(
access_id = access_id,
item_number = item_number,
retry_count = u"3",
), session_id = self.session_id )
# an unknown transaction id might just mean we're still waiting for the transaction to come in,
# so expect a retry
assert result[ u"user" ].username == self.user.username
assert len( result[ u"notebooks" ] ) == 5
notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
assert notebook.object_id == self.notebooks[ 0 ].object_id
assert notebook.name == self.notebooks[ 0 ].name
assert notebook.read_write == True
assert notebook.owner == True
assert notebook.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 not result.get( u"conversion" )
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=4" in result[ u"notes" ][ 0 ].contents
def test_thanks_download_not_yet_paid_with_retry_timeout( self ):
access_id = u"wheeaccessid"
item_number = u"5000"
transaction_id = u"txn"
self.login()
result = self.http_post( "/users/thanks_download", dict(
access_id = access_id,
item_number = item_number,
retry_count = u"16",
), session_id = self.session_id )
# an unknown transaction id might just mean we're still waiting for the transaction to come in,
# so expect a retry
assert result[ u"user" ].username == self.user.username
assert len( result[ u"notebooks" ] ) == 5
notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
assert notebook.object_id == self.notebooks[ 0 ].object_id
assert notebook.name == self.notebooks[ 0 ].name
assert notebook.read_write == True
assert notebook.owner == True
assert notebook.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 not result.get( u"conversion" )
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