async def test_set_password(cli, url, factory: Factory, db_conn, login): await factory.create_company() await factory.create_user() pw_before = await db_conn.fetchval('SELECT password_hash FROM users WHERE id=$1', factory.user_id) with pytest.raises(AssertionError): await login(password='******') data = { 'password1': 'testing-new-password', 'password2': 'testing-new-password', 'token': encrypt_json(cli.app['main_app'], factory.user_id), } r = await cli.json_post(url('set-password'), data=data, origin_null=True) assert r.status == 200, await r.text() data = await r.json() assert data == { 'status': 'success', 'auth_token': RegexStr(r'.+'), 'user': { 'id': factory.user_id, 'first_name': 'Frank', 'last_name': 'Spencer', 'email': '*****@*****.**', 'role': 'admin', 'status': 'active', }, } pw_after = await db_conn.fetchval('SELECT password_hash FROM users WHERE id=$1', factory.user_id) assert pw_after != pw_before await login(password='******', captcha=True)
async def test_set_password_mismatch(cli, url, factory: Factory): await factory.create_company() await factory.create_user() data = { 'password1': 'testing-new-password', 'password2': 'testing-new-password2', 'token': encrypt_json(cli.app['main_app'], factory.user_id), } r = await cli.json_post(url('set-password'), data=data, origin_null=True) assert r.status == 400, await r.text() data = await r.json() assert data == { 'message': 'Invalid Data', 'details': [ { 'loc': [ 'password2', ], 'msg': 'passwords do not match', 'type': 'value_error', }, ], }
async def test_buy_offline_other_admin(cli, url, dummy_server, factory: Factory, login, db_conn): await factory.create_company() await factory.create_cat() await factory.create_user() await factory.create_event(price=10) u2 = await factory.create_user(email='*****@*****.**') await login('*****@*****.**') res: Reservation = await factory.create_reservation(u2) app = cli.app['main_app'] data = dict( booking_token=encrypt_json(app, res.dict()), book_action='buy-tickets-offline', ) r = await cli.json_post(url('event-book-tickets'), data=data) assert r.status == 200, await r.text() assert dummy_server.app['log'] == [ ( 'email_send_endpoint', 'Subject: "The Event Name Ticket Confirmation", To: "Frank Spencer <*****@*****.**>"', ), ] assert 0 == await db_conn.fetchval("SELECT COUNT(*) FROM actions WHERE type='book-free-tickets'") assert 1 == await db_conn.fetchval("SELECT COUNT(*) FROM actions WHERE type='buy-tickets-offline'")
def successful_login(user, app, headers_=None): auth_session = { 'user_id': user['id'], 'user_role': user['role'], 'last_active': int(time()) } auth_token = encrypt_json(app, auth_session) return json_response(status='success', auth_token=auth_token, user=user, headers_=headers_)
async def test_book_offline(donorfy: DonorfyActor, factory: Factory, dummy_server, cli, url, login): await factory.create_company() await factory.create_cat() await factory.create_user(role='host') await factory.create_event(price=10) await login() res = await factory.create_reservation() app = cli.app['main_app'] data = dict(booking_token=encrypt_json(app, res.dict()), book_action='buy-tickets-offline') r = await cli.json_post(url('event-book-tickets'), data=data) assert r.status == 200, await r.text() assert 'POST donorfy_api_root/standard/transactions' not in dummy_server.app['post_data']
async def test_buy_offline_host(cli, url, factory: Factory, login, db_conn): await factory.create_company() await factory.create_cat() await factory.create_user(role='host') await factory.create_event(price=10) await login() res: Reservation = await factory.create_reservation() app = cli.app['main_app'] data = dict(booking_token=encrypt_json(app, res.dict()), book_action='buy-tickets-offline') r = await cli.json_post(url('event-book-tickets'), data=data) assert r.status == 200, await r.text() assert 0 == await db_conn.fetchval("SELECT COUNT(*) FROM actions WHERE type='book-free-tickets'") assert 1 == await db_conn.fetchval("SELECT COUNT(*) FROM actions WHERE type='buy-tickets-offline'") assert 0 == await db_conn.fetchval("SELECT COUNT(*) FROM actions WHERE type='buy-tickets'")
async def test_book_free_with_price(cli, url, factory: Factory): await factory.create_company() await factory.create_cat() await factory.create_user() await factory.create_event(price=10) res: Reservation = await factory.create_reservation() app = cli.app['main_app'] data = dict(booking_token=encrypt_json(app, res.dict()), book_action='book-free-tickets') r = await cli.json_post(url('event-book-tickets'), data=data) assert r.status == 400, await r.text() data = await r.json() assert data == { 'message': 'booking not free', }
async def test_cancel_reservation(cli, url, db_conn, factory: Factory): await factory.create_company() await factory.create_cat() await factory.create_user() await factory.create_event(price=12.5) res = await factory.create_reservation() assert 1 == await db_conn.fetchval('SELECT COUNT(*) FROM tickets') assert 1 == await db_conn.fetchval('SELECT tickets_taken FROM events') booking_token = encrypt_json(cli.app['main_app'], res.dict()) r = await cli.json_post(url('event-cancel-reservation'), data={'booking_token': booking_token}) assert r.status == 200, await r.text() assert 0 == await db_conn.fetchval('SELECT COUNT(*) FROM tickets') assert 0 == await db_conn.fetchval('SELECT tickets_taken FROM events')
async def test_set_password_reuse_token(cli, url, factory: Factory): await factory.create_company() await factory.create_user() data = { 'password1': 'testing-new-password', 'password2': 'testing-new-password', 'token': encrypt_json(cli.app['main_app'], factory.user_id), } r = await cli.json_post(url('set-password'), data=data, origin_null=True) assert r.status == 200, await r.text() r = await cli.json_post(url('set-password'), data=data, origin_null=True) assert r.status == 470, await r.text() data = await r.json() assert data == { 'message': 'This password reset link has already been used.', }
async def test_book_free_no_fee(donorfy: DonorfyActor, factory: Factory, dummy_server, cli, url, login): await factory.create_company() await factory.create_cat() await factory.create_user(role='host') await factory.create_event(price=10) await login() res = await factory.create_reservation() app = cli.app['main_app'] data = dict( booking_token=encrypt_json(app, res.dict()), book_action='buy-tickets-offline', ) r = await cli.json_post(url('event-book-tickets'), data=data) assert r.status == 200, await r.text() trans_data = dummy_server.app['post_data'][ 'POST donorfy_api_root/standard/transactions'] assert len(trans_data) == 1 assert trans_data[0] == { 'ExistingConstituentId': '123456', 'Channel': 'nosht-supper-clubs', 'Currency': 'gbp', 'Campaign': 'supper-clubs:the-event-name', 'PaymentMethod': 'Offline Payment', 'Product': 'Event Ticket(s)', 'Fund': 'Unrestricted General', 'Department': '220 Ticket Sales', 'BankAccount': 'Unrestricted Account', 'DatePaid': CloseToNow(), 'Amount': 10.0, 'ProcessingCostsAmount': 0, 'Quantity': 1, 'Acknowledgement': 'supper-clubs-thanks', 'AcknowledgementText': RegexStr('Ticket ID: .*'), 'Reference': 'Events.HUF:supper-clubs the-event-name', 'AddGiftAidDeclaration': False, 'GiftAidClaimed': False, }
async def test_buy_offline_other_not_admin(cli, url, dummy_server, factory: Factory, login, db_conn): await factory.create_company() await factory.create_cat() await factory.create_user() await factory.create_event(price=10) u2 = await factory.create_user(email='*****@*****.**', role='host') await login('*****@*****.**') res: Reservation = await factory.create_reservation(u2) app = cli.app['main_app'] data = dict(booking_token=encrypt_json(app, res.dict()), book_action='buy-tickets-offline') r = await cli.json_post(url('event-book-tickets'), data=data) assert r.status == 400, await r.text() assert {'message': 'to buy tickets offline you must be the host or an admin'} == await r.json() assert dummy_server.app['log'] == [] assert 0 == await db_conn.fetchval("SELECT COUNT(*) FROM actions WHERE type='book-free-tickets'") assert 0 == await db_conn.fetchval("SELECT COUNT(*) FROM actions WHERE type='buy-tickets-offline'") assert 0 == await db_conn.fetchval("SELECT COUNT(*) FROM actions WHERE type='buy-tickets'")
async def test_waiting_list_book_free(cli, url, login, factory: Factory, db_conn): await factory.create_company() await factory.create_cat() await factory.create_user() await factory.create_event(price=None, status='published') await login() assert await db_conn.fetchval('select count(*) from waiting_list') == 0 r = await cli.json_post(url('event-waiting-list-add', id=factory.event_id)) assert r.status == 200, await r.text() assert await db_conn.fetchval('select count(*) from waiting_list') == 1 res: Reservation = await factory.create_reservation() app = cli.app['main_app'] data = dict(booking_token=encrypt_json(app, res.dict()), book_action='book-free-tickets') r = await cli.json_post(url('event-book-tickets'), data=data) assert r.status == 200, await r.text() assert await db_conn.fetchval('select count(*) from waiting_list') == 0
async def test_stripe_existing_customer_card(cli, db_conn, stripe_factory: Factory): await stripe_factory.create_company(stripe_public_key=stripe_public_key, stripe_secret_key=stripe_secret_key) await stripe_factory.create_cat() await stripe_factory.create_user() await stripe_factory.create_event(ticket_limit=10) res: Reservation = await stripe_factory.create_reservation() app = cli.app['main_app'] customers = await stripe_request(app, BasicAuth(stripe_secret_key), 'get', 'customers?limit=1') customer = customers['data'][0] customer_id = customer['id'] await db_conn.execute('UPDATE users SET stripe_customer_id=$1 WHERE id=$2', customer_id, stripe_factory.user_id) m = StripePayModel( stripe_token='tok_visa', stripe_client_ip='0.0.0.0', stripe_card_ref='{last4}-{exp_year}-{exp_month}'.format(**customer['sources']['data'][0]), booking_token=encrypt_json(app, res.dict()), ) await stripe_pay(m, stripe_factory.company_id, stripe_factory.user_id, app, db_conn) new_customer_id = await db_conn.fetchval('SELECT stripe_customer_id FROM users WHERE id=$1', stripe_factory.user_id) assert new_customer_id == customer_id extra = await db_conn.fetchval("SELECT extra FROM actions WHERE type='buy-tickets'") extra = json.loads(extra) assert extra == { 'new_card': False, 'new_customer': False, 'charge_id': RegexStr('ch_.+'), 'card_expiry': RegexStr('\d+/\d+'), 'card_last4': '4242', }
async def execute(self, m: Model): # noqa: C901 (ignore complexity) event_id = int(self.request.match_info['id']) ticket_count = len(m.tickets) if ticket_count > self.settings.max_tickets: raise JsonErrors.HTTPBadRequest( message='Too many tickets reserved') user_id = self.session['user_id'] status, external_ticket_url, event_name, cover_costs_percentage = await self.conn.fetchrow( """ SELECT e.status, e.external_ticket_url, e.name, c.cover_costs_percentage FROM events AS e JOIN categories c on e.category = c.id WHERE c.company=$1 AND e.id=$2 """, self.request['company_id'], event_id) if status != 'published': raise JsonErrors.HTTPBadRequest(message='Event not published') if external_ticket_url is not None: raise JsonErrors.HTTPBadRequest( message='Cannot reserve ticket for an externally ticketed event' ) r = await self.conn.fetchrow( 'SELECT price FROM ticket_types WHERE event=$1 AND id=$2', event_id, m.ticket_type) if not r: raise JsonErrors.HTTPBadRequest(message='Ticket type not found') item_price, *_ = r if self.settings.ticket_reservation_precheck: # should only be false during CheckViolationError tests tickets_remaining = await self.conn.fetchval( 'SELECT check_tickets_remaining($1, $2)', event_id, self.settings.ticket_ttl) if tickets_remaining is not None and ticket_count > tickets_remaining: raise JsonErrors.HTTP470( message=f'only {tickets_remaining} tickets remaining', tickets_remaining=tickets_remaining) total_price, item_extra_donated = None, None if item_price: total_price = item_price * ticket_count if cover_costs_percentage and m.tickets[0].cover_costs: item_extra_donated = item_price * cover_costs_percentage / 100 total_price += item_extra_donated * ticket_count try: async with self.conn.transaction(): update_user_preferences = await self.create_users(m.tickets) action_id = await record_action_id(self.request, user_id, ActionTypes.reserve_tickets, event_id=event_id) ticket_values = [ Values( email=t.email and t.email.lower(), first_name=t.first_name, last_name=t.last_name, extra_info=t.extra_info or None, ) for t in m.tickets ] await self.conn.execute_b( """ WITH v (email, first_name, last_name, extra_info) AS (VALUES :values) INSERT INTO tickets (event, reserve_action, ticket_type, price, extra_donated, user_id, first_name, last_name, extra_info) SELECT :event, :reserve_action, :ticket_type, :price, :extra_donated, u.id, v.first_name, v.last_name, v.extra_info FROM v LEFT JOIN users AS u ON v.email=u.email AND u.company=:company_id """, event=event_id, reserve_action=action_id, ticket_type=m.ticket_type, price=item_price, extra_donated=item_extra_donated, company_id=self.request['company_id'], values=MultipleValues(*ticket_values), ) await self.conn.execute( 'SELECT check_tickets_remaining($1, $2)', event_id, self.settings.ticket_ttl) except CheckViolationError as exc: if exc.constraint_name != 'ticket_limit_check': # pragma: no branch raise # pragma: no cover logger.warning('CheckViolationError: %s', exc) raise JsonErrors.HTTPBadRequest( message='insufficient tickets remaining') res = Reservation( user_id=user_id, action_id=action_id, price_cent=total_price and int(total_price * 100), event_id=event_id, ticket_count=ticket_count, event_name=event_name, ) if total_price: client_secret = await stripe_buy_intent(res, self.request['company_id'], self.app, self.conn) else: client_secret = None if update_user_preferences: # has to happen after the transactions is finished await self.app['donorfy_actor'].update_user( self.request['session']['user_id'], update_user=False) return { 'booking_token': encrypt_json(self.app, res.dict()), 'action_id': action_id, 'ticket_count': ticket_count, 'item_price': item_price and float(item_price), 'extra_donated': item_extra_donated and float(item_extra_donated * ticket_count), 'total_price': total_price and float(total_price), 'timeout': int(time()) + self.settings.ticket_ttl - 30, 'client_secret': client_secret, }
async def execute(self, m: Model): event_id = int(self.request.match_info['id']) ticket_count = len(m.tickets) if ticket_count < 1: raise JsonErrors.HTTPBadRequest(message='at least one ticket must be purchased') status, event_price, event_name = await self.conn.fetchrow( """ SELECT e.status, e.price, e.name FROM events AS e JOIN categories c on e.category = c.id WHERE c.company=$1 AND e.id=$2 """, self.request['company_id'], event_id ) if status != 'published': raise JsonErrors.HTTPBadRequest(message='Event not published') tickets_remaining = await self.conn.fetchval( 'SELECT check_tickets_remaining($1, $2)', event_id, self.settings.ticket_ttl ) if tickets_remaining is not None and ticket_count > tickets_remaining: raise JsonErrors.HTTP470(message=f'only {tickets_remaining} tickets remaining', tickets_remaining=tickets_remaining) # TODO check user isn't already booked try: async with self.conn.transaction(): user_lookup = await self.create_users(m.tickets) action_id = await record_action_id(self.request, self.session['user_id'], ActionTypes.reserve_tickets) await self.conn.execute_b( 'INSERT INTO tickets (:values__names) VALUES :values', values=MultipleValues(*[ Values( event=event_id, user_id=user_lookup[t.email.lower()] if t.email else None, reserve_action=action_id, extra=to_json_if(t.dict(include={'dietary_req', 'extra_info'})), ) for t in m.tickets ]) ) await self.conn.execute('SELECT check_tickets_remaining($1, $2)', event_id, self.settings.ticket_ttl) except CheckViolationError as e: logger.warning('CheckViolationError: %s', e) raise JsonErrors.HTTPBadRequest(message='insufficient tickets remaining') user = await self.conn.fetchrow( """ SELECT id, full_name(first_name, last_name, email) AS name, email, role FROM users WHERE id=$1 """, self.session['user_id'] ) # TODO needs to work when the event is free price_cent = int(event_price * ticket_count * 100) res = Reservation( user_id=self.session['user_id'], action_id=action_id, price_cent=price_cent, event_id=event_id, ticket_count=ticket_count, event_name=event_name, ) return { 'booking_token': encrypt_json(self.app, res.dict()), 'ticket_count': ticket_count, 'item_price_cent': int(event_price * 100), 'total_price_cent': price_cent, 'user': dict(user), 'timeout': int(time()) + self.settings.ticket_ttl, }
async def test_stripe_successful(cli, db_conn, stripe_factory: Factory): await stripe_factory.create_company(stripe_public_key=stripe_public_key, stripe_secret_key=stripe_secret_key) await stripe_factory.create_cat() await stripe_factory.create_user() await stripe_factory.create_event(ticket_limit=10) res: Reservation = await stripe_factory.create_reservation() app = cli.app['main_app'] m = StripePayModel( stripe_token='tok_visa', stripe_client_ip='0.0.0.0', stripe_card_ref='4242-32-01', booking_token=encrypt_json(app, res.dict()), ) await db_conn.execute('SELECT check_tickets_remaining($1, 10)', res.event_id) customer_id = await db_conn.fetchval('SELECT stripe_customer_id FROM users WHERE id=$1', stripe_factory.user_id) assert customer_id is None ticket_limit, tickets_taken = await db_conn.fetchrow( 'SELECT ticket_limit, tickets_taken FROM events where id=$1', stripe_factory.event_id ) assert (ticket_limit, tickets_taken) == (10, 1) await stripe_pay(m, stripe_factory.company_id, stripe_factory.user_id, app, db_conn) customer_id = await db_conn.fetchval('SELECT stripe_customer_id FROM users WHERE id=$1', stripe_factory.user_id) assert customer_id is not None assert customer_id.startswith('cus_') ticket_limit, tickets_taken = await db_conn.fetchrow( 'SELECT ticket_limit, tickets_taken FROM events where id=$1', stripe_factory.event_id ) assert (ticket_limit, tickets_taken) == (10, 1) paid_action = await db_conn.fetchrow("SELECT * FROM actions WHERE type='buy-tickets'") assert paid_action['company'] == stripe_factory.company_id assert paid_action['user_id'] == stripe_factory.user_id assert paid_action['ts'] == CloseToNow(delta=10) extra = json.loads(paid_action['extra']) assert extra == { 'new_card': True, 'new_customer': True, 'charge_id': RegexStr('ch_.+'), 'card_expiry': RegexStr('\d+/\d+'), 'card_last4': '4242', } charge = await stripe_request(app, BasicAuth(stripe_secret_key), 'get', f'charges/{extra["charge_id"]}') # debug(d) assert charge['amount'] == 10_00 assert charge['description'] == f'1 tickets for Foobar ({stripe_factory.event_id})' assert charge['metadata'] == { 'event': str(stripe_factory.event_id), 'tickets_bought': '1', 'paid_action': str(paid_action['id']), 'reserve_action': str(res.action_id), } assert charge['source']['last4'] == '4242'
async def test_ticket_export(cli, url, login, factory: Factory, db_conn): await factory.create_company() await factory.create_cat() await factory.create_user() await factory.create_event( price=12.34, status='published', location_name='Testing Location', location_lat=51.5, location_lng=-0.5, duration=timedelta(hours=2, minutes=45), ) await factory.buy_tickets(await factory.create_reservation()) await factory.buy_tickets(await factory.create_reservation()) ticket_id = await db_conn.fetchval( 'SELECT id FROM tickets ORDER BY id DESC LIMIT 1') await db_conn.execute('UPDATE tickets SET user_id=NULL WHERE id=$1', ticket_id) admin_user_id = await factory.create_user(email='*****@*****.**', first_name='Admin', last_name='Istrator') await login(email='*****@*****.**') res = await factory.create_reservation(admin_user_id) data = dict(booking_token=encrypt_json(cli.app['main_app'], res.dict()), book_action='buy-tickets-offline') r = await cli.json_post(url('event-book-tickets'), data=data) assert r.status == 200, await r.text() r = await cli.get(url('export', type='tickets')) assert r.status == 200 text = await r.text() data = [dict(r) for r in DictReader(StringIO(text))] ticket_type = str(await db_conn.fetchval('SELECT id FROM ticket_types')) assert data == [ { 'id': RegexStr(r'\d+'), 'ticket_first_name': '', 'ticket_last_name': '', 'status': 'booked', 'booking_action': 'buy-tickets', 'price': '12.34', 'extra_donated': '', 'created_ts': RegexStr(r'\d{4}.*'), 'extra_info': '', 'ticket_type_id': ticket_type, 'ticket_type_name': 'Standard', 'event_id': str(factory.event_id), 'event_slug': 'the-event-name', 'guest_user_id': str(factory.user_id), 'guest_first_name': 'Frank', 'guest_last_name': 'Spencer', 'buyer_user_id': str(factory.user_id), 'buyer_first_name': 'Frank', 'buyer_last_name': 'Spencer', }, { 'id': RegexStr(r'\d+'), 'ticket_first_name': '', 'ticket_last_name': '', 'status': 'booked', 'booking_action': 'buy-tickets', 'price': '12.34', 'extra_donated': '', 'created_ts': RegexStr(r'\d{4}.*'), 'extra_info': '', 'ticket_type_id': ticket_type, 'ticket_type_name': 'Standard', 'event_id': str(factory.event_id), 'event_slug': 'the-event-name', 'guest_user_id': '', 'guest_first_name': '', 'guest_last_name': '', 'buyer_user_id': str(factory.user_id), 'buyer_first_name': 'Frank', 'buyer_last_name': 'Spencer', }, { 'id': RegexStr(r'\d+'), 'ticket_first_name': '', 'ticket_last_name': '', 'status': 'booked', 'booking_action': 'buy-tickets-offline', 'price': '', 'extra_donated': '', 'created_ts': RegexStr(r'\d{4}.*'), 'extra_info': '', 'ticket_type_id': ticket_type, 'ticket_type_name': 'Standard', 'event_id': str(factory.event_id), 'event_slug': 'the-event-name', 'guest_user_id': str(admin_user_id), 'guest_first_name': 'Admin', 'guest_last_name': 'Istrator', 'buyer_user_id': str(admin_user_id), 'buyer_first_name': 'Admin', 'buyer_last_name': 'Istrator', }, ]