def test_email_login_bad_email(self): data = {'log-in.id': '*****@*****.**'} r = self.client.POST('/sign-in', data, raise_immediately=False) assert r.code != 302 assert SESSION not in r.headers.cookie Participant.dequeue_emails() assert not self.get_emails()
def test_payment_providers_of_team(self): # 1. Test when the creator doesn't have any connected payment account. alice = self.make_participant('alice') data = {'name': 'Team1'} r = self.client.PxST('/about/teams', data, auth_as=alice) assert r.code == 302 team = Participant.from_username(data['name']) assert team.payment_providers == 0 # 2. Test when the creator has connected a PayPal account. self.add_payment_account(alice, 'paypal') data = {'name': 'Team2'} r = self.client.PxST('/about/teams', data, auth_as=alice) assert r.code == 302 team = Participant.from_username(data['name']) assert team.payment_providers == 2 # 3. Test after adding a member with a connected Stripe account. bob = self.make_participant('bob') self.add_payment_account(bob, 'stripe') team.add_member(bob) team = team.refetch() assert team.payment_providers == 3 # 4. Test after the creator leaves. team.set_take_for(alice, None, alice) team = team.refetch() assert team.payment_providers == 1
def test_dequeueing_an_email_without_address_just_skips_it(self): larry = self.make_participant('larry') larry.queue_email("verification") assert self.db.one("SELECT spt_name FROM email_queue") == "verification" Participant.dequeue_emails() assert self.mailer.call_count == 0 assert self.db.one("SELECT spt_name FROM email_queue") is None
def test_sign_in(self): r = self.client.PxST('/sign-in', good_data) assert r.code == 302, r.text assert SESSION in r.headers.cookie Participant.dequeue_emails() assert self.get_last_email() p = Participant.from_username(good_data['sign-in.username']) assert p.avatar_url
def test_email_login_team_account(self): email = '*****@*****.**' self.make_participant('team', email=email, kind='group') data = {'log-in.id': email} r = self.client.POST('/log-in', data, raise_immediately=False) assert SESSION not in r.headers.cookie Participant.dequeue_emails() assert not self.get_emails()
def authenticate_user_if_possible(request, response, state, user, _): """This signs the user in. """ if request.line.uri.startswith(b'/assets/'): return if not state['website'].db: return # Cookie and form auth # We want to try cookie auth first, but we want form auth to supersede it p = None if SESSION in request.headers.cookie: creds = request.headers.cookie[SESSION].value.split(':', 2) if len(creds) == 2: creds = [creds[0], 1, creds[1]] if len(creds) == 3: p = Participant.authenticate(*creds) if p: state['user'] = p session_p, p = p, None session_suffix = '' redirect_url = request.line.uri.decoded if request.method == 'POST': body = _get_body(request) if body: p = sign_in_with_form_data(body, state) carry_on = body.pop('log-in.carry-on', None) if not p and carry_on: p_email = session_p and ( session_p.email or session_p.get_any_email() ) if p_email != carry_on: state['log-in.carry-on'] = carry_on raise LoginRequired redirect_url = body.get('sign-in.back-to') or redirect_url elif request.method == 'GET' and request.qs.get('log-in.id'): id = request.qs.pop('log-in.id') session_id = request.qs.pop('log-in.key', 1) token = request.qs.pop('log-in.token', None) p = Participant.authenticate(id, session_id, token) if not p and (not session_p or session_p.id != id): raise response.error(400, _("This login link is expired or invalid.")) else: qs = '?' + urlencode(request.qs, doseq=True) if request.qs else '' redirect_url = request.path.raw + qs session_p = p session_suffix = '.em' if p: if session_p: session_p.sign_out(response.headers.cookie) if p.status == 'closed': p.update_status('active') if not p.session: p.sign_in(response.headers.cookie, suffix=session_suffix) state['user'] = p if request.body.pop('form.repost', None) != 'true': response.redirect(redirect_url, trusted_url=False)
def test_dbtd_distributes_balance_as_final_gift(self): alice = self.make_participant('alice', balance=EUR('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, EUR('3.00')) alice.set_tip_to(carl, EUR('2.00')) alice.distribute_balances_to_donees() assert Participant.from_username('bob').balance == EUR('6.00') assert Participant.from_username('carl').balance == EUR('4.00') assert Participant.from_username('alice').balance == EUR('0.00')
def test_dbtd_gives_all_to_claimed(self): alice = self.make_participant('alice', balance=EUR('10.00')) bob = self.make_participant('bob') carl = self.make_stub() alice.set_tip_to(bob, EUR('3.00')) alice.set_tip_to(carl, EUR('2.00')) alice.distribute_balances_to_donees() assert Participant.from_id(bob.id).balance == EUR('10.00') assert Participant.from_id(carl.id).balance == EUR('0.00') assert Participant.from_id(alice.id).balance == EUR('0.00')
def test_dbtd_favors_highest_tippee_in_rounding_errors(self): alice = self.make_participant('alice', balance=EUR('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, EUR('3.00')) alice.set_tip_to(carl, EUR('6.00')) alice.distribute_balances_to_donees() assert Participant.from_username('bob').balance == EUR('3.33') assert Participant.from_username('carl').balance == EUR('6.67') assert Participant.from_username('alice').balance == EUR('0.00')
def notify_participants(self): previous_ts_end = self.db.one( """ SELECT ts_end FROM paydays WHERE ts_start < %s ORDER BY ts_end DESC LIMIT 1 """, (self.ts_start,), default=constants.BIRTHDAY, ) # Income notifications r = self.db.all( """ SELECT tippee, json_agg(t) AS transfers FROM transfers t WHERE "timestamp" > %s AND "timestamp" <= %s GROUP BY tippee """, (previous_ts_end, self.ts_end), ) for tippee_id, transfers in r: successes = [t for t in transfers if t["status"] == "succeeded"] if not successes: continue by_team = {k: sum(t["amount"] for t in v) for k, v in group_by(successes, "team").items()} personal = by_team.pop(None, 0) by_team = {Participant.from_id(k).username: v for k, v in by_team.items()} Participant.from_id(tippee_id).notify( "income", total=sum(t["amount"] for t in successes), personal=personal, by_team=by_team ) # Low-balance notifications participants = self.db.all( """ SELECT p.*::participants FROM participants p WHERE balance < giving AND giving > 0 AND EXISTS ( SELECT 1 FROM transfers t WHERE t.tipper = p.id AND t.timestamp > %s AND t.timestamp <= %s AND t.status = 'succeeded' ) """, (previous_ts_end, self.ts_end), ) for p in participants: p.notify("low_balance")
def notify_participants(self): previous_ts_end = self.db.one(""" SELECT ts_end FROM paydays WHERE ts_start < %s ORDER BY ts_end DESC LIMIT 1 """, (self.ts_start,), default=constants.BIRTHDAY) # Income notifications r = self.db.all(""" SELECT tippee, json_agg(t) AS transfers FROM transfers t WHERE "timestamp" > %s AND "timestamp" <= %s GROUP BY tippee """, (previous_ts_end, self.ts_end)) for tippee_id, transfers in r: successes = [t for t in transfers if t['status'] == 'succeeded'] if not successes: continue by_team = {k: sum(t['amount'] for t in v) for k, v in group_by(successes, 'team').items()} personal = by_team.pop(None, 0) by_team = {Participant.from_id(k).username: v for k, v in by_team.items()} Participant.from_id(tippee_id).notify( 'income', total=sum(t['amount'] for t in successes), personal=personal, by_team=by_team, ) # Low-balance notifications participants = self.db.all(""" SELECT p.*::participants FROM participants p WHERE balance < ( SELECT sum(amount) FROM current_tips t JOIN participants p2 ON p2.id = t.tippee WHERE t.tipper = p.id AND p2.mangopay_user_id IS NOT NULL AND p2.status = 'active' ) AND EXISTS ( SELECT 1 FROM transfers t WHERE t.tipper = p.id AND t.timestamp > %s AND t.timestamp <= %s AND t.status = 'succeeded' ) """, (previous_ts_end, self.ts_end)) for p in participants: p.notify('low_balance')
def test_can_post_to_close_page(self): alice = self.make_participant('alice', balance=7) bob = self.make_participant('bob') alice.set_tip_to(bob, D('10.00')) data = {'disburse_to': 'downstream'} response = self.client.PxST('/alice/settings/close', auth_as=alice, data=data) assert response.code == 302 assert response.headers['Location'] == '/alice/' assert Participant.from_username('alice').balance == 0 assert Participant.from_username('bob').balance == 7
def test_dbtd_skips_stopped_tips(self): alice = self.make_participant('alice', balance=EUR('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, EUR('1.00')) alice.stop_tip_to(bob) alice.set_tip_to(carl, EUR('2.00')) alice.distribute_balances_to_donees() assert Participant.from_username('bob').balance == EUR('0.00') assert Participant.from_username('carl').balance == EUR('10.00') assert Participant.from_username('alice').balance == EUR('0.00')
def test_dbafg_favors_highest_tippee_in_rounding_errors(self): alice = self.make_participant('alice', balance=D('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, D('3.00')) alice.set_tip_to(carl, D('6.00')) with self.db.get_cursor() as cursor: alice.distribute_balance_as_final_gift(cursor) assert Participant.from_username('bob').balance == D('3.33') assert Participant.from_username('carl').balance == D('6.67') assert Participant.from_username('alice').balance == D('0.00')
def test_dbtd_needs_claimed_tips(self): alice = self.make_participant('alice', balance=EUR('10.00')) bob = self.make_stub() carl = self.make_stub() alice.set_tip_to(bob, EUR('3.00')) alice.set_tip_to(carl, EUR('2.00')) with pytest.raises(UnableToDistributeBalance): alice.distribute_balances_to_donees() assert Participant.from_id(bob.id).balance == EUR('0.00') assert Participant.from_id(carl.id).balance == EUR('0.00') assert Participant.from_id(alice.id).balance == EUR('10.00')
def test_dbtd_with_zero_balance_is_a_noop(self): alice = self.make_participant('alice', balance=EUR('0.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, EUR('3.00')) alice.set_tip_to(carl, EUR('6.00')) alice.distribute_balances_to_donees() assert self.db.one("SELECT count(*) FROM tips") == 2 assert Participant.from_username('bob').balance == EUR('0.00') assert Participant.from_username('carl').balance == EUR('0.00') assert Participant.from_username('alice').balance == EUR('0.00')
def test_dbafg_gives_all_to_claimed(self): alice = self.make_participant('alice', balance=D('10.00')) bob = self.make_participant('bob') carl = self.make_stub() alice.set_tip_to(bob, D('3.00')) alice.set_tip_to(carl, D('2.00')) with self.db.get_cursor() as cursor: alice.distribute_balance_as_final_gift(cursor) assert Participant.from_id(bob.id).balance == D('10.00') assert Participant.from_id(carl.id).balance == D('0.00') assert Participant.from_id(alice.id).balance == D('0.00')
def test_dbafg_distributes_balance_as_final_gift(self): alice = self.make_participant('alice', balance=D('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, D('3.00')) alice.set_tip_to(carl, D('2.00')) with self.db.get_cursor() as cursor: alice.distribute_balance_as_final_gift(cursor) assert Participant.from_username('bob').balance == D('6.00') assert Participant.from_username('carl').balance == D('4.00') assert Participant.from_username('alice').balance == D('0.00')
def test_can_dequeue_an_email(self): larry = self.make_participant('larry', email='*****@*****.**') larry.queue_email("verification", link='https://example.com/larry') assert self.db.one("SELECT spt_name FROM email_queue") == "verification" Participant.dequeue_emails() assert self.mailer.call_count == 1 last_email = self.get_last_email() assert last_email['to'][0] == 'larry <*****@*****.**>' expected = "connect larry" assert expected in last_email['text'] assert self.db.one("SELECT spt_name FROM email_queue") is None
def test_iter_payday_events(self): Payday.start().run() team = self.make_participant('team', kind='group') alice = self.make_participant('alice') self.make_exchange('mango-cc', 10000, 0, alice) self.make_exchange('mango-cc', -5000, 0, alice) self.db.run(""" UPDATE transfers SET timestamp = "timestamp" - interval '1 month' """) bob = self.make_participant('bob') carl = self.make_participant('carl') david = self.make_participant('david') self.make_exchange('mango-cc', 10000, 0, david) david.set_tip_to(team, Decimal('1.00')) team.set_take_for(bob, Decimal('1.00'), bob) alice.set_tip_to(bob, Decimal('5.00')) assert bob.balance == 0 for i in range(2): Payday.start().run() self.db.run(""" UPDATE paydays SET ts_start = ts_start - interval '1 week' , ts_end = ts_end - interval '1 week'; UPDATE transfers SET timestamp = "timestamp" - interval '1 week'; """) bob = Participant.from_id(bob.id) assert bob.balance == 12 Payday().start() events = list(iter_payday_events(self.db, bob)) assert len(events) == 9 assert events[0]['kind'] == 'totals' assert events[0]['given'] == 0 assert events[0]['received'] == 12 assert events[1]['kind'] == 'day-open' assert events[1]['payday_number'] == 2 assert events[2]['balance'] == 12 assert events[-1]['kind'] == 'day-close' assert events[-1]['balance'] == 0 alice = Participant.from_id(alice.id) assert alice.balance == 4990 events = list(iter_payday_events(self.db, alice)) assert events[0]['given'] == 10 assert len(events) == 11 carl = Participant.from_id(carl.id) assert carl.balance == 0 events = list(iter_payday_events(self.db, carl)) assert len(events) == 0
def test_payday_moves_money(self): self.janet.set_tip_to(self.homer, EUR('6.00')) # under $10! self.make_exchange('mango-cc', 10, 0, self.janet) Payday.start().run() janet = Participant.from_username('janet') homer = Participant.from_username('homer') assert homer.balance == EUR('6.00') assert janet.balance == EUR('4.00') assert self.transfer_mock.call_count
def test_dbafg_skips_zero_tips(self): alice = self.make_participant('alice', balance=D('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, D('0.00')) alice.set_tip_to(carl, D('2.00')) with self.db.get_cursor() as cursor: alice.distribute_balance_as_final_gift(cursor) assert self.db.one("SELECT count(*) FROM tips WHERE tippee=%s", (bob.id,)) == 1 assert Participant.from_username('bob').balance == D('0.00') assert Participant.from_username('carl').balance == D('10.00') assert Participant.from_username('alice').balance == D('0.00')
def test_dbafg_with_zero_balance_is_a_noop(self): alice = self.make_participant('alice', balance=D('0.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, D('3.00')) alice.set_tip_to(carl, D('6.00')) with self.db.get_cursor() as cursor: alice.distribute_balance_as_final_gift(cursor) assert self.db.one("SELECT count(*) FROM tips") == 2 assert Participant.from_username('bob').balance == D('0.00') assert Participant.from_username('carl').balance == D('0.00') assert Participant.from_username('alice').balance == D('0.00')
def test_participant_can_modify_privacy_settings(self): # turn them all on self.hit_edit(data=ALL_ON) alice = Participant.from_id(self.alice.id) for k in PRIVACY_FIELDS: assert getattr(alice, k) in (1, 3, True) # turn them all off self.hit_edit(data=ALL_OFF) alice = Participant.from_id(self.alice.id) for k in PRIVACY_FIELDS: assert getattr(alice, k) in (0, 2, False)
def test_dbafg_needs_claimed_tips(self): alice = self.make_participant('alice', balance=D('10.00')) bob = self.make_stub() carl = self.make_stub() alice.set_tip_to(bob, D('3.00')) alice.set_tip_to(carl, D('2.00')) with self.db.get_cursor() as cursor: with pytest.raises(alice.NoOneToGiveFinalGiftTo): alice.distribute_balance_as_final_gift(cursor) assert Participant.from_id(bob.id).balance == D('0.00') assert Participant.from_id(carl.id).balance == D('0.00') assert Participant.from_id(alice.id).balance == D('10.00')
def test_sync_with_mangopay_transfers(self): self.make_exchange('mango-cc', 10, 0, self.janet) with mock.patch('liberapay.billing.exchanges.record_transfer_result') as rtr: rtr.side_effect = Foobar() with self.assertRaises(Foobar): transfer(self.db, self.janet.id, self.david.id, D('10.00'), 'tip') t = self.db.one("SELECT * FROM transfers") assert t.status == 'pre' sync_with_mangopay(self.db) t = self.db.one("SELECT * FROM transfers") assert t.status == 'succeeded' assert Participant.from_username('david').balance == 10 assert Participant.from_username('janet').balance == 0
def sign_in_with_form_data(body, state): p = None _, website = state['_'], state['website'] if body.get('log-in.id'): id = body.pop('log-in.id') k = 'email' if '@' in id else 'username' p = Participant.authenticate( k, 'password', id, body.pop('log-in.password') ) if not p: state['sign-in.error'] = _("Bad username or password.") if p and p.status == 'closed': p.update_status('active') elif body.get('sign-in.username'): if body.pop('sign-in.terms') != 'agree': raise Response(400, 'you have to agree to the terms') kind = body.pop('sign-in.kind') if kind not in ('individual', 'organization'): raise Response(400, 'bad kind') with website.db.get_cursor() as c: p = Participant.make_active( body.pop('sign-in.username'), kind, body.pop('sign-in.password'), cursor=c ) p.add_email(body.pop('sign-in.email'), cursor=c) p.authenticated = True elif body.get('email-login.email'): email = body.pop('email-login.email') p = Participant._from_thing('email', email) if p: p.start_session() qs = {'log-in.id': p.id, 'log-in.token': p.session_token} p.send_email( 'password_reset', email=email, link=p.url('settings/', qs), link_validity=SESSION_TIMEOUT, ) state['email-login.sent-to'] = email else: state['sign-in.error'] = _( "We didn't find any account whose primary email address is {0}.", email ) p = None return p
def test_sync_with_mangopay_deletes_transfers_that_didnt_happen(self): self.make_exchange('mango-cc', 10, 0, self.janet) with mock.patch('liberapay.billing.exchanges.record_transfer_result') as rtr \ , mock.patch('liberapay.billing.mangoapi.transfers.Create') as Create: rtr.side_effect = Create.side_effect = Foobar with self.assertRaises(Foobar): transfer(self.db, self.janet.id, self.david.id, D('10.00'), 'tip') t = self.db.one("SELECT * FROM transfers") assert t.status == 'pre' sync_with_mangopay(self.db) transfers = self.db.all("SELECT * FROM transfers") assert not transfers assert Participant.from_username('david').balance == 0 assert Participant.from_username('janet').balance == 10
def test_changes_to_others_take_can_increase_members_take(self): team, alice, bob = self.make_team_of_two() self.take_last_week(team, alice, '30.00') team.set_take_for(alice, D('25.00'), alice) self.take_last_week(team, bob, '50.00') team.set_take_for(bob, D('100.00'), bob) alice = Participant.from_username('alice') assert alice.receiving == alice.taking == 20 team.set_take_for(bob, D('75.00'), bob) alice = Participant.from_username('alice') assert alice.receiving == alice.taking == 25
def test_take_over_sends_notifications_to_patrons(self): dan_twitter = self.make_elsewhere('twitter', 1, 'dan') self.alice.set_tip_to(self.dan, '100') # Alice shouldn't receive an email. self.bob.set_tip_to(dan_twitter, '100') # Bob should receive an email. self.dan.take_over(dan_twitter, have_confirmation=True) Participant.dequeue_emails() assert self.mailer.call_count == 1 last_email = self.get_last_email() assert last_email['to'][0] == 'bob <*****@*****.**>' assert "to dan" in last_email['text'] assert "Change your email settings" in last_email['text']
def test_verify_email(self): self.hit_email_spt('add-email', '*****@*****.**') nonce = self.alice.get_email('*****@*****.**').nonce self.hit_verify('*****@*****.**', nonce) alice = Participant.from_username('alice') assert alice.email == '*****@*****.**' # Add and verify a second email address a year later, the primary email # address should stay the same. self.db.run("UPDATE emails SET added_time = added_time - interval '1 year'") self.hit_email_spt('add-email', '*****@*****.**') nonce = self.alice.get_email('*****@*****.**').nonce self.hit_verify('*****@*****.**', nonce) alice = alice.refetch() assert alice.email == '*****@*****.**'
def authenticate_user_if_possible(request, user): """This signs the user in. """ if request.line.uri.startswith('/assets/'): return if 'Authorization' in request.headers: header = request.headers['authorization'] if not header.startswith('Basic '): raise Response(401, 'Unsupported authentication method') try: creds = binascii.a2b_base64(header[len('Basic '):]).split(':', 1) except binascii.Error: raise Response(400, 'Malformed "Authorization" header') participant = Participant.authenticate('id', 'password', *creds) if not participant: raise Response(401) _turn_off_csrf(request) return {'user': participant} elif SESSION in request.headers.cookie: creds = request.headers.cookie[SESSION].value.split(':', 1) p = Participant.authenticate('id', 'session', *creds) if p: return {'user': p}
def test_verify_email_expired_nonce(self): address = '*****@*****.**' self.hit_email_spt('add-email', address) self.db.run( """ UPDATE emails SET added_time = (now() - INTERVAL '25 hours') WHERE participant = %s """, (self.alice.id, )) nonce = self.alice.get_email(address).nonce r = self.alice.verify_email(address, nonce) assert r == emails.VERIFICATION_EXPIRED actual = Participant.from_username('alice').email assert actual == None
def transfer(db, tipper, tippee, amount, context, **kw): tipper_wallet = NS(remote_id=kw.get('tipper_wallet_id'), remote_owner_id=kw.get('tipper_mango_id')) if not all(tipper_wallet.__dict__.values()): tipper_wallet = Participant.from_id(tipper).get_current_wallet(amount.currency) tippee_wallet = NS(remote_id=kw.get('tippee_wallet_id'), remote_owner_id=kw.get('tippee_mango_id')) if not all(tippee_wallet.__dict__.values()): tippee_wallet = Participant.from_id(tippee).get_current_wallet(amount.currency, create=True) wallet_from = tipper_wallet.remote_id wallet_to = tippee_wallet.remote_id t_id = prepare_transfer( db, tipper, tippee, amount, context, wallet_from, wallet_to, team=kw.get('team'), invoice=kw.get('invoice'), bundles=kw.get('bundles'), unit_amount=kw.get('unit_amount'), ) tr = Transfer() tr.AuthorId = tipper_wallet.remote_owner_id tr.CreditedUserId = tippee_wallet.remote_owner_id tr.CreditedWalletId = wallet_to tr.DebitedFunds = amount.int() tr.DebitedWalletId = wallet_from tr.Fees = Money(0, amount.currency) tr.Tag = str(t_id) return execute_transfer(db, t_id, tr), t_id
def test_changes_to_others_take_affects_members_take(self): team, alice, bob = self.make_team_of_two() self.take_last_week(team, alice, '30.00') team.set_take_for(alice, D('40.00'), alice) self.take_last_week(team, bob, '50.00') team.set_take_for(bob, D('60.00'), bob) alice = Participant.from_username('alice') assert alice.receiving == alice.taking == 40 for m in team.get_members().values(): assert m['nominal_take'] == m['actual_amount']
def test_cannot_update_email_to_already_verified(self): bob = self.make_participant('bob') self.alice.add_email('*****@*****.**') nonce = self.alice.get_email('*****@*****.**').nonce r = self.alice.verify_email('*****@*****.**', nonce) assert r == emails.VERIFICATION_SUCCEEDED with self.assertRaises(EmailAlreadyTaken): bob.add_email('*****@*****.**') nonce = bob.get_email('*****@*****.**').nonce bob.verify_email('*****@*****.**', nonce) email_alice = Participant.from_username('alice').email assert email_alice == '*****@*****.**'
def test_cannot_update_email_to_already_verified(self): bob = self.make_participant('bob') self.alice.add_email('*****@*****.**') email_row = self.alice.get_email('*****@*****.**') r = self.alice.verify_email(email_row.id, email_row.nonce, ANON) assert r == EmailVerificationResult.SUCCEEDED with self.assertRaises(EmailAlreadyTaken): bob.add_email('*****@*****.**') nonce = bob.get_email('*****@*****.**').nonce bob.hit_verify('*****@*****.**', nonce) email_alice = Participant.from_username('alice').email assert email_alice == '*****@*****.**'
def test_email_login(self): email = '*****@*****.**' alice = self.make_participant('alice') alice.add_email(email) alice.close(None) data = {'log-in.id': email.upper()} r = self.client.POST('/', data, raise_immediately=False) alice = alice.refetch() assert alice.session_token not in r.headers.raw.decode('ascii') assert alice.session_token not in r.body.decode('utf8') Participant.dequeue_emails() last_email = self.get_last_email() assert last_email and last_email['subject'] == 'Log in to Liberapay' assert 'log-in.token=' + alice.session_token in last_email['text'] url = '/alice/?foo=bar&log-in.id=%s&log-in.token=%s' r = self.client.GxT(url % (alice.id, alice.session_token)) alice2 = alice.refetch() assert alice2.session_token != alice.session_token # ↑ this means that the link is only valid once assert r.code == 302 assert r.headers[b'Location'] == b'http://localhost/alice/?foo=bar' # ↑ checks that original path and query are preserved # Check that we can change our password password = '******' r = self.client.POST( '/alice/settings/edit', {'new-password': password}, cookies=r.headers.cookie, raise_immediately=False, ) assert r.code == 302 alice2 = Participant.authenticate('id', 'password', alice.id, password) assert alice2 and alice2 == alice
def test_email_login_with_old_unverified_address(self): email = '*****@*****.**' alice = self.make_participant('alice', email=None) alice.add_email(email) Participant.dequeue_emails() self.db.run("UPDATE emails SET nonce = null") # Initiate email log-in data = {'log-in.id': email.upper()} r = self.client.POST('/', data, raise_immediately=False) session = self.db.one( "SELECT * FROM user_secrets WHERE participant = %s", (alice.id, )) assert session.secret not in r.headers.raw.decode('ascii') assert session.secret not in r.body.decode('utf8') # Check the email message Participant.dequeue_emails() last_email = self.get_last_email() assert last_email and last_email['subject'] == 'Log in to Liberapay' email_row = alice.get_email(email) assert email_row.verified is None assert email_row.nonce qs = 'log-in.id=%i&log-in.key=%i&log-in.token=%s&email.id=%s&email.nonce=%s' % ( alice.id, session.id, session.secret, email_row.id, email_row.nonce) assert qs in last_email['text'] # Log in r = self.client.GxT('/alice/?' + qs) assert r.code == 302 assert r.headers[b'Location'].startswith(b'http://localhost/alice/') # Check that the email address is now verified email_row = alice.get_email(email) assert email_row.verified alice = alice.refetch() assert alice.email == email
def test_2_sync_with_mangopay_handles_payins_that_didnt_happen(self): pass # this is for pep8 with mock.patch('liberapay.billing.transactions.record_exchange_result') as rer, \ mock.patch('liberapay.billing.transactions.DirectPayIn.save', autospec=True) as save: rer.side_effect = save.side_effect = Foobar with self.assertRaises(Foobar): charge(self.db, self.janet_route, EUR('33.67'), 'http://localhost/') exchange = self.db.one("SELECT * FROM exchanges") assert exchange.status == 'pre' self.throw_transactions_back_in_time() sync_with_mangopay(self.db) exchange = self.db.one("SELECT * FROM exchanges") assert exchange.status == 'failed' assert exchange.note == 'interrupted' assert Participant.from_username('janet').balance == 0
def test_sign_in_through_donation_form(self): alice = self.make_participant('alice', accepted_currencies=None) extra = { 'amount': '10000', 'currency': 'KRW', 'period': 'weekly', 'form.repost': 'true' } r = self.sign_in(url='/~1/tip', extra=extra) assert r.code == 302, r.text assert r.headers[b'Location'].startswith( b'http://localhost/bob/giving/') bob = Participant.from_username('bob') tip = bob.get_tip_to(alice) assert tip.amount == Money('10000', 'KRW') assert bob.main_currency == 'KRW'
def check(): alice = Participant.from_username('alice') bob = Participant.from_username('bob') carl = Participant.from_username('carl') dana = Participant.from_username('dana') emma = Participant.from_username('emma') assert alice.giving == EUR('10.69') assert alice.receiving == EUR('0.00') assert alice.npatrons == 0 assert alice.nteampatrons == 0 assert bob.giving == EUR('0.00') assert bob.taking == EUR('1.00') assert bob.receiving == EUR('7.00') assert bob.npatrons == 1 assert bob.nteampatrons == 1 assert carl.giving == EUR('0.00') assert carl.receiving == EUR('0.00') assert carl.npatrons == 0 assert carl.nteampatrons == 0 assert dana.receiving == EUR('3.49') assert dana.npatrons == 1 assert dana.nteampatrons == 1 assert emma.receiving == EUR('0.50') assert emma.npatrons == 1 assert emma.nteampatrons == 0 funded_tips = self.db.all( "SELECT amount FROM tips WHERE is_funded ORDER BY id") assert funded_tips == [ 3, 6, 0.5, EUR('1.20'), EUR('0.49'), EUR('2.22') ] team = Participant.from_username('team') assert team.receiving == EUR('1.20') assert team.npatrons == 1 assert team.leftover == EUR('0.20') team2 = Participant.from_username('team2') assert team2.receiving == EUR('0.49') assert team2.npatrons == 1 assert team2.leftover == EUR('0.00') janet = self.janet.refetch() assert janet.giving == 0 assert janet.receiving == 0 assert janet.taking == 0 assert janet.npatrons == 0 assert janet.nteampatrons == 0
def test_disavow_email(self): self.client.PxST( '/sign-up', { 'sign-in.email': '*****@*****.**', 'sign-in.username': '******', 'sign-in.currency': 'USD', }) bob = Participant.from_username('bob') email = bob.get_email('*****@*****.**') url = '/bob/emails/disavow?email.id=%s&email.nonce=%s' % (email.id, email.nonce) verification_email = self.get_last_email() assert url in verification_email['text'] r = self.client.GET(url) assert r.code == 200 email = bob.get_email(email.address) assert email.disavowed is True assert email.disavowed_time is not None assert email.verified is None assert email.verified_time is None assert email.nonce # Check idempotency r = self.client.GET(url) assert r.code == 200 # Check that resending the verification email isn't allowed r = self.client.POST( '/bob/emails/modify.json', {'resend': email.address}, auth_as=bob, raise_immediately=False, ) assert r.code == 400, r.text # Test adding the address to the blacklist r = self.client.POST(url, {'action': 'add_to_blacklist'}) assert r.code == 200, r.text with self.assertRaises(EmailAddressIsBlacklisted): check_email_blacklist(email.address) # and removing it r = self.client.POST(url, {'action': 'remove_from_blacklist'}) assert r.code == 200, r.text assert check_email_blacklist(email.address) is None
def record_payout_refund(db, payout_refund): orig_payout = BankWirePayOut.get(payout_refund.InitialTransactionId) e_origin = db.one("SELECT * FROM exchanges WHERE id = %s" % (orig_payout.Tag,)) e_refund_id = db.one("SELECT id FROM exchanges WHERE refund_ref = %s", (e_origin.id,)) if e_refund_id: # Already recorded return e_refund_id amount, fee, vat = -e_origin.amount, -e_origin.fee, -e_origin.vat assert payout_refund.DebitedFunds == Money(int(amount * 100), 'EUR') assert payout_refund.Fees == Money(int(fee * 100), 'EUR') route = ExchangeRoute.from_id(e_origin.route) participant = Participant.from_id(e_origin.participant) return db.one(""" INSERT INTO exchanges (amount, fee, vat, participant, status, route, note, refund_ref) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING id """, (amount, fee, vat, participant.id, 'created', route.id, None, e_origin.id))
def refetch_repos(): with website.db.get_cursor() as cursor: repo = cursor.one(""" SELECT r.participant, r.platform FROM repositories r WHERE r.info_fetched_at < now() - interval '6 days' AND r.participant IS NOT NULL ORDER BY r.info_fetched_at ASC LIMIT 1 """) if not repo: return participant = Participant.from_id(repo.participant) account = participant.get_account_elsewhere(repo.platform) sess = account.get_auth_session() start_time = utcnow() logger.debug( "Refetching repository data for participant ~%s from %s account %s" % (participant.id, account.platform, account.user_id)) next_page = None for i in range(10): r = account.platform_data.get_repos(account, page_url=next_page, sess=sess) upsert_repos(cursor, r[0], participant, utcnow()) next_page = r[2].get('next') if not next_page: break sleep(1) deleted_count = cursor.one( """ WITH deleted AS ( DELETE FROM repositories WHERE participant = %s AND platform = %s AND info_fetched_at < %s RETURNING id ) SELECT count(*) FROM deleted """, (participant.id, account.platform, start_time)) event_type = 'fetch_repos:%s' % account.id payload = dict(partial_list=bool(next_page), deleted_count=deleted_count) participant.add_event(cursor, event_type, payload)
def make_payin_and_transfers( self, route, amount, transfers, status='succeeded', error=None, payer_country=None, remote_id='fake', ): payer = route.participant payin = prepare_payin(self.db, payer, amount, route) payin = update_payin(self.db, payin.id, remote_id, status, error) provider = route.network.split('-', 1)[0] payin_transfers = [] for tippee, pt_amount, opt in transfers: try: destination = resolve_destination(self.db, tippee, provider, payer, payer_country, pt_amount) except MissingPaymentAccount as e: destination = self.add_payment_account(e.args[0], provider) recipient = Participant.from_id(destination.participant) if tippee.kind == 'group': context = 'team-donation' team = tippee.id else: context = 'personal-donation' team = None pt = prepare_payin_transfer(self.db, payin, recipient, destination, context, pt_amount, opt.get('unit_amount'), opt.get('period'), team) pt = update_payin_transfer(self.db, pt.id, opt.get('remote_id', 'fake'), opt.get('status', status), opt.get('error', error)) payin_transfers.append(pt) tippee.update_receiving() if team: recipient.update_receiving() payer.update_giving() return payin, payin_transfers
def record_unexpected_payin(db, payin): """Record an unexpected bank wire payin. """ assert payin.PaymentType == 'BANK_WIRE' debited_amount = payin.DebitedFunds / Decimal(100) paid_fee = payin.Fees / Decimal(100) vat = skim_bank_wire(debited_amount)[2] wallet_id = payin.CreditedWalletId participant = Participant.from_mangopay_user_id(payin.AuthorId) current_wallet = participant.get_current_wallet(debited_amount.currency) assert current_wallet.remote_id == wallet_id route = ExchangeRoute.upsert_bankwire_route(participant) amount = debited_amount - paid_fee return db.one(""" INSERT INTO exchanges (amount, fee, vat, participant, status, route, note, remote_id, wallet_id) VALUES (%s, %s, %s, %s, 'created', %s, NULL, %s, %s) RETURNING id """, (amount, paid_fee, vat, participant.id, route.id, payin.Id, wallet_id))
def transfer(db, tipper, tippee, amount, context, **kw): t_id = db.one(""" INSERT INTO transfers (tipper, tippee, amount, context, team, invoice, status) VALUES (%s, %s, %s, %s, %s, %s, 'pre') RETURNING id """, (tipper, tippee, amount, context, kw.get('team'), kw.get('invoice'))) get = lambda id, col: db.one("SELECT {0} FROM participants WHERE id = %s".format(col), (id,)) tr = Transfer() tr.AuthorId = kw.get('tipper_mango_id') or get(tipper, 'mangopay_user_id') tr.CreditedUserId = kw.get('tippee_mango_id') or get(tippee, 'mangopay_user_id') tr.CreditedWalletId = kw.get('tippee_wallet_id') or get(tippee, 'mangopay_wallet_id') if not tr.CreditedWalletId: tr.CreditedWalletId = create_wallet(db, Participant.from_id(tippee)) tr.DebitedFunds = Money(int(amount * 100), 'EUR') tr.DebitedWalletId = kw.get('tipper_wallet_id') or get(tipper, 'mangopay_wallet_id') tr.Fees = Money(0, 'EUR') tr.Tag = str(t_id) tr.save() return record_transfer_result(db, t_id, tr)
def record_payout_refund(db, payout_refund): orig_payout = BankWirePayOut.get(payout_refund.InitialTransactionId) e_origin = db.one("SELECT * FROM exchanges WHERE id = %s", (orig_payout.Tag,)) e_refund_id = db.one("SELECT id FROM exchanges WHERE refund_ref = %s", (e_origin.id,)) if e_refund_id: # Already recorded return e_refund_id amount, fee, vat = -e_origin.amount, -e_origin.fee, -e_origin.vat assert payout_refund.DebitedFunds / 100 == amount assert payout_refund.Fees / 100 == fee route = ExchangeRoute.from_id(e_origin.route) participant = Participant.from_id(e_origin.participant) remote_id = payout_refund.Id wallet_id = e_origin.wallet_id return db.one(""" INSERT INTO exchanges (amount, fee, vat, participant, status, route, note, refund_ref, remote_id, wallet_id) VALUES (%s, %s, %s, %s, 'created', %s, NULL, %s, %s, %s) RETURNING id """, (amount, fee, vat, participant.id, route.id, e_origin.id, remote_id, wallet_id))
def test_migrate(self): # Step 1 r = self.client.POST('/migrate', initial_data) assert r.code == 200 assert "Welcome, alice!" in r.text, r.text # Step 2 cache_entry = resources.__cache__[abspath('www/migrate.spt')] simplate_context = cache_entry.resource.pages[0] requests = MagicMock() requests.post.return_value = gratipay_response with patch.dict(simplate_context, {'requests': requests}): r = self.client.PxST('/migrate?step=2', initial_data, sentry_reraise=False) assert r.code == 302 assert r.headers[b'Location'] == b'?step=3' # Step 3 alice = Participant.from_id(1) r = self.client.GET('/migrate?step=3', auth_as=alice) assert r.code == 200
def test_create_close_and_reopen_team(self): alice = self.make_participant('alice') r = self.client.PxST('/about/teams', {'name': 'Team'}, auth_as=alice) assert r.code == 302 assert r.headers[b'Location'] == b'/Team/edit' t = Participant.from_username('Team') assert t assert t.status == 'active' assert t.nmembers == 1 t.close() t2 = t.refetch() assert t.status == t2.status == 'closed' assert t.goal == t2.goal == -1 r = self.client.PxST('/about/teams', {'name': 'Team'}, auth_as=alice) assert r.code == 302 assert r.headers[b'Location'] == b'/Team/edit' t = t.refetch() assert t.nmembers == 1 assert t.status == 'active' assert t.goal == None
def test_epi_deletes_personal_information(self): alice = self.make_participant( 'alice', hide_giving=True, hide_receiving=True, avatar_url='img-url', email='*****@*****.**', ) alice.upsert_statement('en', 'not forgetting to be awesome!') alice.add_email('*****@*****.**') alice.erase_personal_information() new_alice = Participant.from_username('alice') assert alice.get_statement(['en']) == (None, None) assert alice.hide_giving == new_alice.hide_giving == True assert alice.hide_receiving == new_alice.hide_receiving == True assert alice.avatar_url == new_alice.avatar_url == None assert alice.email == new_alice.email emails = alice.get_emails() assert len(emails) == 1 assert emails[0].address == '*****@*****.**' assert emails[0].verified
def return_payin_bundles_to_origin(db, exchange, last_resort_payer, create_debts=True): """Transfer money linked to a specific payin back to the original owner. """ currency = exchange.amount.currency chargebacks_account = Participant.get_chargebacks_account(currency)[0] original_owner = exchange.participant origin_wallet = db.one("SELECT * FROM wallets WHERE remote_id = %s", (exchange.wallet_id,)) transfer_kw = dict( tippee_wallet_id=origin_wallet.remote_id, tippee_mango_id=origin_wallet.remote_owner_id, ) payin_bundles = db.all(""" SELECT * FROM cash_bundles WHERE origin = %s AND disputed = true """, (exchange.id,)) grouped = group_by(payin_bundles, lambda b: (b.owner, b.withdrawal)) for (current_owner, withdrawal), bundles in grouped.items(): assert current_owner != chargebacks_account.id if current_owner == original_owner: continue amount = sum(b.amount for b in bundles) if current_owner is None: if not last_resort_payer or not create_debts: continue bundles = None withdrawer = db.one("SELECT participant FROM exchanges WHERE id = %s", (withdrawal,)) payer = last_resort_payer.id create_debt(db, withdrawer, payer, amount, exchange.id) create_debt(db, original_owner, withdrawer, amount, exchange.id) else: bundles = [b.id for b in bundles] payer = current_owner if create_debts: create_debt(db, original_owner, payer, amount, exchange.id) transfer(db, payer, original_owner, amount, 'chargeback', bundles=bundles, **transfer_kw)
def test_cpi_clears_personal_information(self): alice = self.make_participant( 'alice', goal=EUR(100), hide_giving=True, hide_receiving=True, avatar_url='img-url', email='*****@*****.**', session_token='deadbeef', session_expires='2000-01-01', giving=EUR(20), receiving=EUR(40), npatrons=21, ) alice.upsert_statement('en', 'not forgetting to be awesome!') alice.add_email('*****@*****.**') with self.db.get_cursor() as cursor: alice.clear_personal_information(cursor) new_alice = Participant.from_username('alice') assert alice.get_statement(['en']) == (None, None) assert alice.goal == new_alice.goal == None assert alice.hide_giving == new_alice.hide_giving == True assert alice.hide_receiving == new_alice.hide_receiving == True assert alice.avatar_url == new_alice.avatar_url == None assert alice.email == new_alice.email assert alice.giving == new_alice.giving == 0 assert alice.receiving == new_alice.receiving == 0 assert alice.npatrons == new_alice.npatrons == 0 assert alice.session_token == new_alice.session_token == None assert alice.session_expires.year == new_alice.session_expires.year == date.today( ).year emails = alice.get_emails() assert len(emails) == 1 assert emails[0].address == '*****@*****.**' assert emails[0].verified
def test_reopening_closed_account(self): alice = self.make_participant( 'alice', hide_giving=True, hide_receiving=True, avatar_url='img-url', email='*****@*****.**', ) alice.update_goal(EUR(100)) alice.upsert_statement('en', 'not forgetting to be awesome!') alice.add_email('*****@*****.**') alice.close(None) alice.update_status('active') new_alice = Participant.from_username('alice') assert alice.get_statement(['en']).content assert alice.goal == new_alice.goal == EUR(100) assert alice.hide_giving == new_alice.hide_giving == True assert alice.hide_receiving == new_alice.hide_receiving == True assert alice.avatar_url == new_alice.avatar_url == 'img-url' assert alice.email == new_alice.email emails = alice.get_emails() assert len(emails) == 2
def test_verified_email_is_not_changed_after_update(self): self.add_and_verify_email('*****@*****.**') self.alice.add_email('*****@*****.**') expected = '*****@*****.**' actual = Participant.from_username('alice').email assert expected == actual
def refund_payin(db, exchange, create_debts=False, refund_fee=False, dry_run=False): """Refund a specific payin. """ assert exchange.status == 'succeeded' and exchange.remote_id, exchange e_refund = db.one("SELECT e.* FROM exchanges e WHERE e.refund_ref = %s", (exchange.id,)) if e_refund and e_refund.status == 'succeeded': return 'already done', e_refund # Lock the bundles and try to swap them with db.get_cursor() as cursor: cursor.run("LOCK TABLE cash_bundles IN EXCLUSIVE MODE") bundles = [NS(d._asdict()) for d in cursor.all(""" UPDATE cash_bundles SET disputed = true WHERE origin = %s RETURNING * """, (exchange.id,))] bundles_sum = sum(b.amount for b in bundles) assert bundles_sum == exchange.amount original_owner = exchange.participant for b in bundles: if b.owner == original_owner: continue try_to_swap_bundle(cursor, b, original_owner) # Move the funds back to the original wallet LiberapayOrg = Participant.from_username('LiberapayOrg') assert LiberapayOrg return_payin_bundles_to_origin(db, exchange, LiberapayOrg, create_debts) # Add a debt for the fee if create_debts and refund_fee: create_debt(db, original_owner, LiberapayOrg.id, exchange.fee, exchange.id) # Compute and check the amount wallet = db.one("SELECT * FROM wallets WHERE remote_id = %s", (exchange.wallet_id,)) if e_refund and e_refund.status == 'pre': amount = -e_refund.amount else: amount = min(wallet.balance, exchange.amount) if amount <= 0: return ('not enough money: wallet balance = %s' % wallet.balance), None # Stop here if this is a dry run zero = exchange.fee.zero() fee, vat = (exchange.fee, exchange.vat) if refund_fee else (zero, zero) if dry_run: msg = ( '[dry run] full refund of payin #%s (liberapay id %s): amount = %s, fee = %s' % (exchange.remote_id, exchange.id, exchange.amount, exchange.fee) ) if amount + fee == exchange.amount + exchange.fee else ( '[dry run] partial refund of payin #%s (liberapay id %s): %s of %s, fee %s of %s' % (exchange.remote_id, exchange.id, amount, exchange.amount, fee, exchange.fee) ) return msg, None # Record the refund attempt participant = Participant.from_id(exchange.participant) if not (e_refund and e_refund.status == 'pre'): with db.get_cursor() as cursor: cursor.run("LOCK TABLE cash_bundles IN EXCLUSIVE MODE") bundles = [NS(d._asdict()) for d in cursor.all(""" SELECT * FROM cash_bundles WHERE origin = %s AND wallet_id = %s AND disputed = true """, (exchange.id, exchange.wallet_id))] e_refund = cursor.one(""" INSERT INTO exchanges (participant, amount, fee, vat, route, status, refund_ref, wallet_id) VALUES (%s, %s, %s, %s, %s, 'pre', %s, %s) RETURNING * """, (participant.id, -amount, -fee, -vat, exchange.route, exchange.id, exchange.wallet_id)) propagate_exchange(cursor, participant, e_refund, None, e_refund.amount, bundles=bundles) # Submit the refund m_refund = PayInRefund(payin_id=exchange.remote_id) m_refund.AuthorId = wallet.remote_owner_id m_refund.Tag = str(e_refund.id) m_refund.DebitedFunds = amount.int() m_refund.Fees = -fee.int() try: m_refund.save() except Exception as e: error = repr_exception(e) e_refund = record_exchange_result(db, e_refund.id, '', 'failed', error, participant) return 'exception', e_refund e_refund = record_exchange_result( db, e_refund.id, m_refund.Id, m_refund.Status.lower(), repr_error(m_refund), participant ) return e_refund.status, e_refund
def test_patrons_are_charged_after_pledgee_joins(self): bob = self.make_participant('bob', email='*****@*****.**') dan = self.make_participant('dan', email='*****@*****.**') alice = self.make_participant('alice', email='*****@*****.**') dan_twitter = self.make_elsewhere('twitter', 1, 'dan') alice.set_tip_to(dan, EUR('100'), renewal_mode=2) bob.set_tip_to(dan_twitter, EUR('100'), renewal_mode=2) dan.take_over(dan_twitter, have_confirmation=True) # dan hasn't connected any payment account yet, so nothing should happen Participant.notify_patrons() Participant.dequeue_emails() assert self.mailer.call_count == 0 # add a payment account and check again, but it's still too early self.add_payment_account(dan, 'stripe') Participant.notify_patrons() Participant.dequeue_emails() assert self.mailer.call_count == 0 # simulate skipping one day ahead, now there should be a notification # and a scheduled payin self.db.run("UPDATE events SET ts = ts - interval '24 hours'") Participant.notify_patrons() Participant.dequeue_emails() assert self.mailer.call_count == 1 last_email = self.get_last_email() assert last_email['to'][0] == 'bob <*****@*****.**>' assert last_email[ 'subject'] == "dan from Twitter has joined Liberapay!" scheduled_payins = self.db.all( "SELECT * FROM scheduled_payins WHERE payer = %s", (bob.id, )) assert len(scheduled_payins) == 1 assert scheduled_payins[0].amount == EUR('400.00') assert scheduled_payins[0].automatic is True # check that the notification isn't sent again self.mailer.reset_mock() Participant.notify_patrons() Participant.dequeue_emails() assert self.mailer.call_count == 0
def test_patrons_are_notified_after_pledgee_joins(self): self.bob = self.make_participant('bob', email='*****@*****.**') self.dan = self.make_participant('dan', email='*****@*****.**') self.alice = self.make_participant('alice', email='*****@*****.**') dan_twitter = self.make_elsewhere('twitter', 1, 'dan') self.alice.set_tip_to(self.dan, EUR('100')) # Alice shouldn't receive an email. self.bob.set_tip_to(dan_twitter, EUR('100')) # Bob should receive an email. self.dan.take_over(dan_twitter, have_confirmation=True) # dan hasn't connected any payment account yet, so there shouldn't be a notification Participant.notify_patrons() Participant.dequeue_emails() assert self.mailer.call_count == 0 # add a payment account and check again, but it's still too early self.add_payment_account(self.dan, 'stripe') Participant.notify_patrons() Participant.dequeue_emails() assert self.mailer.call_count == 0 # simulate skipping one day ahead, now there should be a notification self.db.run("UPDATE events SET ts = ts - interval '24 hours'") Participant.notify_patrons() Participant.dequeue_emails() assert self.mailer.call_count == 1 last_email = self.get_last_email() assert last_email['to'][0] == 'bob <*****@*****.**>' assert "to dan" in last_email['text'] assert "Change your email settings" in last_email['text'] # check that the notification isn't sent again self.mailer.reset_mock() Participant.notify_patrons() Participant.dequeue_emails() assert self.mailer.call_count == 0
def test_update_cached_amounts(self): team = self.make_participant('team', kind='group') alice = self.make_participant('alice') alice_card = self.upsert_route(alice, 'stripe-card') bob = self.make_participant('bob') carl = self.make_participant('carl') carl_card = self.upsert_route(carl, 'stripe-card') dana = self.make_participant('dana') emma = Participant.make_stub(username='******') team2 = self.make_participant('team2', kind='group') team2.add_member(dana) alice.set_tip_to(dana, EUR('3.00')) self.make_payin_and_transfer(alice_card, dana, EUR('30.00')) alice.set_tip_to(bob, EUR('6.00')) self.make_payin_and_transfer(alice_card, bob, EUR('60.00')) alice.set_tip_to(emma, EUR('0.50')) alice.set_tip_to(team, EUR('1.20')) alice.set_tip_to(team2, EUR('0.49')) self.make_payin_and_transfer(alice_card, team2, EUR('4.90')) bob.set_tip_to(alice, EUR('5.00')) team.set_take_for(bob, EUR('1.00'), team) self.make_payin_and_transfer(alice_card, team, EUR('12.00')) bob.set_tip_to(dana, EUR('2.00')) # funded by bob's take bob.set_tip_to(emma, EUR('7.00')) # not funded, insufficient receiving carl.set_tip_to(dana, EUR('2.08')) # not funded, insufficient balance self.make_payin_and_transfer(carl_card, dana, EUR('1.56')) fred = self.make_participant('fred') fred_card = self.upsert_route(fred, 'stripe-card') fred.set_tip_to(dana, EUR('2.22')) self.make_payin_and_transfer(fred_card, dana, EUR('8.88')) self.db.run( "UPDATE participants SET is_suspended = true WHERE username = '******'" ) dana.update_receiving() def check(): alice = Participant.from_username('alice') bob = Participant.from_username('bob') carl = Participant.from_username('carl') dana = Participant.from_username('dana') emma = Participant.from_username('emma') assert alice.giving == EUR('10.69') assert alice.receiving == EUR('0.00') assert alice.npatrons == 0 assert alice.nteampatrons == 0 assert bob.giving == EUR('0.00') assert bob.taking == EUR('1.00') assert bob.receiving == EUR('7.00') assert bob.npatrons == 1 assert bob.nteampatrons == 1 assert carl.giving == EUR('0.00') assert carl.receiving == EUR('0.00') assert carl.npatrons == 0 assert carl.nteampatrons == 0 assert dana.receiving == EUR('3.49') assert dana.npatrons == 1 assert dana.nteampatrons == 1 assert emma.receiving == EUR('0.50') assert emma.npatrons == 1 assert emma.nteampatrons == 0 funded_tips = self.db.all( "SELECT amount FROM tips WHERE is_funded ORDER BY id") assert funded_tips == [ 3, 6, 0.5, EUR('1.20'), EUR('0.49'), EUR('2.22') ] team = Participant.from_username('team') assert team.receiving == EUR('1.20') assert team.npatrons == 1 assert team.leftover == EUR('0.20') team2 = Participant.from_username('team2') assert team2.receiving == EUR('0.49') assert team2.npatrons == 1 assert team2.leftover == EUR('0.00') janet = self.janet.refetch() assert janet.giving == 0 assert janet.receiving == 0 assert janet.taking == 0 assert janet.npatrons == 0 assert janet.nteampatrons == 0 # Pre-test check check() # Check that update_cached_amounts doesn't mess anything up Payday.start().update_cached_amounts() check() # Check that update_cached_amounts actually updates amounts self.db.run(""" UPDATE tips t SET is_funded = true FROM participants p WHERE p.id = t.tippee AND p.mangopay_user_id IS NOT NULL; UPDATE participants SET giving = (10000,'EUR') , taking = (10000,'EUR') WHERE mangopay_user_id IS NOT NULL; UPDATE participants SET npatrons = 10000 , receiving = (10000,'EUR'); """) Payday.start().update_cached_amounts() check() # Check that the update methods of Participant concur for p in self.db.all("SELECT p.*::participants FROM participants p"): p.update_receiving() p.update_giving() check()