refactored tests to use the stripe API more and use a list of accounts in the test context.

pull/183/head
Chris Veilleux 2019-06-12 15:15:40 -05:00
parent b0f7f8cbf7
commit 869e7ece42
14 changed files with 252 additions and 341 deletions

View File

@ -12,11 +12,3 @@ Feature: Add a new account
And user does not specify an email address
When the new account request is submitted
Then the request will fail with a bad request error
Scenario: Successful on-boarding with membership
Given a new account without a membership
And user with username bar is authenticated
When a user opts into a membership during on-boarding
Then the request will be successful
And the account will be updated with the membership

View File

@ -2,10 +2,11 @@ Feature: Pair a device
Test the device add endpoint
Scenario: Add a device
Given user with username foo is authenticated
And a device pairing code
When an API request is sent to add a device
Then the request will be successful
And the device is added to the database
And the pairing code is removed from cache
And the pairing token is added to cache
Given an account
And the account is authenticated
And a device pairing code
When an API request is sent to add a device
Then the request will be successful
And the device is added to the database
And the pairing code is removed from cache
And the pairing token is added to cache

View File

@ -9,18 +9,24 @@ Feature: Authentication with JWTs
be the only place authentication logic needs to be tested.
Scenario: Request for user data includes valid access token
Given user with username foo is authenticated
Given an account with a valid access token
When a user requests their profile
Then the request will be successful
And the authentication tokens will remain unchanged
Scenario: Access token expired
Given an authenticated user with an expired access token
Given an account with an expired access token
When a user requests their profile
Then the request will be successful
And the authentication tokens will be refreshed
Scenario: Access token missing but refresh token valid
Given an account with a refresh token but no access token
When a user requests their profile
Then the request will be successful
And the authentication tokens will be refreshed
Scenario: Both access and refresh tokens expired
Given a previously authenticated user with expired tokens
Given an account with expired access and refresh tokens
When a user requests their profile
Then the request will fail with an unauthorized error

View File

@ -1,16 +1,9 @@
from behave import fixture, use_fixture
from account_api.api import acct
from selene.data.account import (
AccountRepository,
)
from selene.testing import (
add_account,
add_account_geography,
add_agreements,
remove_account,
remove_agreements
)
from selene.testing.account import add_account, remove_account
from selene.testing.account_geography import add_account_geography
from selene.testing.agreement import add_agreements, remove_agreements
from selene.util.cache import SeleneCache
from selene.util.db import connect_to_db
@ -38,31 +31,22 @@ def after_all(context):
def before_scenario(context, _):
context.account = context.foo_account = add_account(context.db)
context.geography_id = add_account_geography(context.db, context.account)
account = add_account(context.db)
context.accounts = dict(foo=account)
context.geography_id = add_account_geography(context.db, account)
def after_scenario(context, _):
db = connect_to_db(context.client_config['DB_CONNECTION_CONFIG'])
_delete_account(context, db)
_clean_cache()
def _delete_account(context, db):
"""Delete the account and all its related data.
"""Scenario-level cleanup.
The database is setup with cascading deletes that take care of cleaning up[
referential integrity for us. All we have to do here is delete the account
and all rows on all tables related to that account will also be deleted.
"""
acct_repository = AccountRepository(db)
remove_account(db, context.foo_account)
bar_acct = acct_repository.get_account_by_email('bar@mycroft.ai')
if bar_acct is not None:
remove_account(db, bar_acct)
test_acct = acct_repository.get_account_by_email('test@mycroft.ai')
if test_acct is not None:
remove_account(db, test_acct)
for account in context.accounts.values():
remove_account(context.db, account)
_clean_cache()
def _clean_cache():

View File

@ -3,7 +3,29 @@ Feature: Manage account profiles
settings.
Scenario: Retrieve authenticated user's account
Given user with username foo is authenticated
When a user requests their profile
Then the request will be successful
And user profile is returned
Given an account with a monthly membership
# And the account is authenticated
When a user requests their profile
Then the request will be successful
And user profile is returned
Scenario: user with free account opts into a membership
Given an account without a membership
And the account is authenticated
When a monthly membership is added
Then the request will be successful
And the account should have a monthly membership
Scenario: user opts out monthly membership
Given an account with a monthly membership
# And the account is authenticated
When the membership is cancelled
Then the request will be successful
And the account should have no membership
Scenario: user changes from a monthly membership to yearly membership
Given an account with a monthly membership
# And the account is authenticated
When the membership is changed to yearly
Then the request will be successful
And the account should have a yearly membership

View File

@ -2,7 +2,7 @@ Feature: Delete an account
Test the API call to delete an account and all its related data from the database.
Scenario: Successful account deletion
Given user with username foo is authenticated
Given an account with a monthly membership
When the user's account is deleted
Then the request will be successful
And the membership is removed from stripe

View File

@ -1,19 +1,15 @@
from binascii import b2a_base64
from datetime import date, datetime
from dataclasses import dataclass
from unittest.mock import call, patch
from datetime import date
from behave import given, then, when
from flask import json
from hamcrest import assert_that, equal_to, is_in, none, not_none
from hamcrest import assert_that, equal_to, is_in, not_none
from selene.data.account import (
AccountRepository,
MONTHLY_MEMBERSHIP,
PRIVACY_POLICY,
TERMS_OF_USE
)
from selene.testing.account import add_account
new_account_request = dict(
termsOfUse=True,
@ -27,50 +23,16 @@ new_account_request = dict(
)
@dataclass
class StripeMock(object):
"""Object intended to behave like stripe Customer/Subscription create"""
id: str
@given('a user completes new account setup')
def build_new_account_request(context):
context.new_account_request = new_account_request
@given('user opts out of membership')
def add_maybe_later_membership(context):
context.new_account_request['support'].update(
membership=None,
paymentMethod=None,
paymentAccountId=None
)
@given('user opts into a membership')
def change_membership_option(context):
context.new_account_request['support'].update(
membership='Monthly Membership',
paymentMethod='Stripe',
paymentToken='tok_visa'
)
@given('user does not specify an email address')
def remove_email_from_request(context):
del(context.new_account_request['login']['email'])
@given('a new account without a membership')
def add_account_no_membership(context):
new_account_overrides = dict(
email_address='bar@mycroft.ai',
username='bar',
membership=None
)
context.bar_account = add_account(context.db, **new_account_overrides)
@when('the new account request is submitted')
def call_add_account_endpoint(context):
context.client.content_type = 'application/json'
@ -81,44 +43,12 @@ def call_add_account_endpoint(context):
)
@when('a user opts into a membership during on-boarding')
def call_update_account_endpoint(context):
onboarding_request = dict(
membership=dict(
newMembership=True,
membershipType=MONTHLY_MEMBERSHIP,
paymentToken='test_payment_token',
paymentMethod='Stripe'
)
)
with patch('stripe.Subscription.create') as subscription_patch:
subscription_patch.return_value = StripeMock(id='test_subscription')
with patch('stripe.Customer.create') as customer_patch:
customer_patch.return_value = StripeMock(id='test_customer')
context.response = context.client.patch(
'/api/account',
data=json.dumps(onboarding_request),
content_type='application/json'
)
assert_that(
customer_patch.mock_calls,
equal_to([call(
email='bar@mycroft.ai',
source='test_payment_token'
)])
)
assert_that(
subscription_patch.mock_calls,
equal_to([call(
customer='test_customer',
items=[{'plan': 'monthly_premium'}])
]))
@then('the account will be added to the system')
def check_db_for_account(context):
acct_repository = AccountRepository(context.db)
account = acct_repository.get_account_by_email('bar@mycroft.ai')
# add account to context so it will deleted by cleanup step
context.accounts['bar'] = account
assert_that(account, not_none())
assert_that(
account.email_address, equal_to('bar@mycroft.ai')
@ -128,14 +58,3 @@ def check_db_for_account(context):
for agreement in account.agreements:
assert_that(agreement.type, is_in((PRIVACY_POLICY, TERMS_OF_USE)))
assert_that(agreement.accept_date, equal_to(str(date.today())))
@then('the account will be updated with the membership')
def validate_membership_update(context):
account_repository = AccountRepository(context.db)
membership = account_repository.get_active_account_membership(
context.account.id
)
assert_that(membership.type, equal_to(MONTHLY_MEMBERSHIP))
assert_that(membership.start_date, equal_to(datetime.utcnow().date()))
assert_that(membership.end_date, none())

View File

@ -1,7 +1,7 @@
import json
from behave import given, when, then
from hamcrest import assert_that, equal_to, has_key, none, not_none
from hamcrest import assert_that, equal_to, none, not_none
from selene.data.device import DeviceRepository
from selene.util.cache import SeleneCache
@ -26,6 +26,11 @@ def set_device_pairing_code(context):
context.pairing_code = 'ABC123'
@given('an account')
def define_account(context):
context.username = 'foo'
@when('an API request is sent to add a device')
def add_device(context):
device = dict(
@ -57,6 +62,7 @@ def validate_pairing_code_removal(context):
@then('the device is added to the database')
def validate_response(context):
device_id = context.response.data.decode()
account = context.accounts['foo']
db = connect_to_db(context.client_config['DB_CONNECTION_CONFIG'])
device_repository = DeviceRepository(db)
device = device_repository.get_device_by_id(device_id)
@ -64,7 +70,7 @@ def validate_response(context):
assert_that(device, not_none())
assert_that(device.name, equal_to('home'))
assert_that(device.placement, equal_to('kitchen'))
assert_that(device.account_id, equal_to(context.account.id))
assert_that(device.account_id, equal_to(account.id))
@then('the pairing token is added to cache')

View File

@ -1,27 +1,61 @@
from behave import given, then
from hamcrest import assert_that, equal_to, is_not
from selene.api.testing import (
from selene.testing.api import (
generate_access_token,
generate_refresh_token,
set_access_token_cookie,
set_refresh_token_cookie,
validate_token_cookies
)
from selene.data.account import AccountRepository
from selene.util.auth import AuthenticationToken
from selene.util.db import connect_to_db
EXPIRE_IMMEDIATELY = 0
@given('an authenticated user with an expired access token')
def generate_refresh_token_only(context):
generate_access_token(context, expire=True)
generate_refresh_token(context)
@given('an account with a valid access token')
def use_account_with_valid_access_token(context):
context.username = 'foo'
context.access_token = generate_access_token(context)
set_access_token_cookie(context)
context.refresh_token = generate_refresh_token(context)
set_refresh_token_cookie(context)
@given('an account with an expired access token')
def generate_expired_access_token(context):
context.username = 'foo'
context.access_token = generate_access_token(
context,
duration=EXPIRE_IMMEDIATELY
)
set_access_token_cookie(context, duration=EXPIRE_IMMEDIATELY)
context.refresh_token = generate_refresh_token(context)
set_refresh_token_cookie(context)
context.old_refresh_token = context.refresh_token.jwt
@given('a previously authenticated user with expired tokens')
@given('an account with a refresh token but no access token')
def generate_refresh_token_only(context):
context.username = 'foo'
context.refresh_token = generate_refresh_token(context)
set_refresh_token_cookie(context)
context.old_refresh_token = context.refresh_token.jwt
@given('an account with expired access and refresh tokens')
def expire_both_tokens(context):
generate_access_token(context, expire=True)
generate_refresh_token(context, expire=True)
context.username = 'foo'
context.access_token = generate_access_token(
context,
duration=EXPIRE_IMMEDIATELY
)
set_access_token_cookie(context, duration=EXPIRE_IMMEDIATELY)
context.refresh_token = generate_refresh_token(
context,
duration=EXPIRE_IMMEDIATELY
)
set_refresh_token_cookie(context, duration=EXPIRE_IMMEDIATELY)
@then('the authentication tokens will remain unchanged')
@ -37,10 +71,6 @@ def check_for_new_cookies(context):
context.refresh_token,
is_not(equal_to(context.old_refresh_token))
)
db = connect_to_db(context.client_config['DB_CONNECTION_CONFIG'])
acct_repository = AccountRepository(db)
account = acct_repository.get_account_by_id(context.account.id)
refresh_token = AuthenticationToken(
context.client_config['REFRESH_SECRET'],
0
@ -49,4 +79,6 @@ def check_for_new_cookies(context):
refresh_token.validate()
assert_that(refresh_token.is_valid, equal_to(True))
assert_that(refresh_token.is_expired, equal_to(False))
assert_that(refresh_token.account_id, equal_to(account.id))
assert_that(
refresh_token.account_id,
equal_to(context.accounts['foo'].id))

View File

@ -3,13 +3,20 @@ from http import HTTPStatus
from behave import given, then
from hamcrest import assert_that, equal_to, is_in
from selene.api.testing import generate_access_token, generate_refresh_token
from selene.testing.api import (
generate_access_token,
generate_refresh_token,
set_access_token_cookie,
set_refresh_token_cookie
)
@given('user with username {username} is authenticated')
def setup_authenticated_user(context, username):
generate_access_token(context, username)
generate_refresh_token(context, username)
@given('the account is authenticated')
def use_account_with_valid_access_token(context):
context.access_token = generate_access_token(context)
set_access_token_cookie(context)
context.refresh_token = generate_refresh_token(context)
set_refresh_token_cookie(context)
@then('the request will be successful')

View File

@ -1,9 +1,40 @@
import json
from datetime import date
from behave import then, when
from hamcrest import assert_that, equal_to, has_item, none
from behave import given, then, when
from hamcrest import assert_that, equal_to, has_item, none, starts_with
from selene.data.account import PRIVACY_POLICY
from selene.data.account import AccountRepository, PRIVACY_POLICY
from selene.testing.api import (
generate_access_token,
generate_refresh_token,
set_access_token_cookie,
set_refresh_token_cookie
)
from selene.testing.membership import MONTHLY_MEMBERSHIP, YEARLY_MEMBERSHIP
BAR_EMAIL_ADDRESS = 'bar@mycroft.ai'
STRIPE_METHOD = 'Stripe'
VISA_TOKEN = 'tok_visa'
@given('an account with a monthly membership')
def add_membership_to_account(context):
"""Use the API to add a monthly membership on Stripe
The API is used so that the Stripe API can be interacted with.
"""
context.username = 'foo'
context.access_token = generate_access_token(context)
set_access_token_cookie(context)
context.refresh_token = generate_refresh_token(context)
set_refresh_token_cookie(context)
add_membership_via_api(context)
@given('an account without a membership')
def get_account_no_membership(context):
context.username = 'foo'
@when('a user requests their profile')
@ -14,12 +45,58 @@ def call_account_endpoint(context):
)
@when('a monthly membership is added')
def add_monthly_membership(context):
context.response = add_membership_via_api(context)
@when('the membership is cancelled')
def cancel_membership(context):
membership_data = dict(
newMembership=False,
membershipType=None
)
context.response = context.client.patch(
'/api/account',
data=json.dumps(dict(membership=membership_data)),
content_type='application/json'
)
def add_membership_via_api(context):
membership_data = dict(
newMembership=True,
membershipType=MONTHLY_MEMBERSHIP,
paymentMethod=STRIPE_METHOD,
paymentToken=VISA_TOKEN
)
return context.client.patch(
'/api/account',
data=json.dumps(dict(membership=membership_data)),
content_type='application/json'
)
@when('the membership is changed to yearly')
def change_to_yearly_account(context):
membership_data = dict(
newMembership=False,
membershipType=YEARLY_MEMBERSHIP
)
context.response = context.client.patch(
'/api/account',
data=json.dumps(dict(membership=membership_data)),
content_type='application/json'
)
@then('user profile is returned')
def validate_response(context):
response_data = context.response.json
account = context.accounts['foo']
assert_that(
response_data['emailAddress'],
equal_to(context.account.email_address)
equal_to(account.email_address)
)
assert_that(
response_data['membership']['type'],
@ -38,3 +115,37 @@ def validate_response(context):
equal_to(str(date.today().strftime('%B %d, %Y')))
)
assert_that(agreement, has_item('id'))
@then('the account should have a monthly membership')
def validate_monthly_account(context):
acct_repository = AccountRepository(context.db)
membership = acct_repository.get_active_account_membership(
context.accounts['foo'].id
)
assert_that(
membership.type,
equal_to(MONTHLY_MEMBERSHIP)
)
assert_that(membership.payment_account_id, starts_with('cus'))
assert_that(membership.start_date, equal_to(date.today()))
assert_that(membership.end_date, none())
@then('the account should have no membership')
def validate_absence_of_membership(context):
acct_repository = AccountRepository(context.db)
membership = acct_repository.get_active_account_membership(
context.accounts['foo'].id
)
assert_that(membership, none())
@then('the account should have a yearly membership')
def yearly_account(context):
acct_repository = AccountRepository(context.db)
membership = acct_repository.get_active_account_membership(
context.accounts['foo'].id
)
assert_that(membership.type, equal_to(YEARLY_MEMBERSHIP))
assert_that(membership.payment_account_id, starts_with('cus'))

View File

@ -1,30 +1,30 @@
import os
from unittest.mock import call, patch
import stripe
from behave import then, when
from hamcrest import assert_that, equal_to, not_none
from hamcrest import assert_that, equal_to
from stripe.error import InvalidRequestError
from selene.data.account import AccountRepository
@when('the user\'s account is deleted')
def account_deleted(context):
with patch('stripe.Subscription') as stripe_patch:
context.response = context.client.delete('/api/account')
assert_that(
stripe_patch.mock_calls,
equal_to([call.retrieve('bar'), call.retrieve().delete()])
)
acct_repository = AccountRepository(context.db)
membership = acct_repository.get_active_account_membership(
context.accounts['foo'].id
)
context.accounts['foo'].membership = membership
context.response = context.client.delete('/api/account')
@then('the membership is removed from stripe')
def check_stripe(context):
stripe_id = context.account.membership.payment_account_id
assert_that(stripe_id, not_none())
account = context.accounts['foo']
stripe.api_key = os.environ['STRIPE_PRIVATE_KEY']
subscription_not_found = False
try:
stripe.Subscription.retrieve(stripe_id)
stripe.Subscription.retrieve(account.membership.payment_account_id)
except InvalidRequestError:
subscription_not_found = True
assert_that(subscription_not_found, equal_to(True))

View File

@ -1,150 +0,0 @@
from binascii import b2a_base64
from datetime import date
import json
from behave import given, when, then
from hamcrest import assert_that, equal_to, starts_with, none
from selene.api.testing import generate_access_token, generate_refresh_token
from selene.data.account import (
AccountRepository,
Account,
AccountAgreement,
PRIVACY_POLICY
)
from selene.util.db import connect_to_db
TEST_EMAIL_ADDRESS = 'test@mycroft.ai'
new_account_request = dict(
username='test',
termsOfUse=True,
privacyPolicy=True,
login=dict(
federatedPlatform=None,
federatedToken=None,
email=b2a_base64(b'test@mycroft.ai').decode(),
password=b2a_base64(b'12345678').decode()
),
support=dict(
openDataset=True,
membership='Maybe Later',
paymentMethod=None,
paymentAccountId=None
)
)
MONTHLY_MEMBERSHIP = 'Monthly Membership'
STRIPE_METHOD = 'Stripe'
VISA_TOKEN = 'tok_visa'
YEARLY_MEMBERSHIP = 'Yearly Membership'
@given('a user with a free account')
def create_account(context):
context.account = Account(
email_address='test@mycroft.ai',
username='test',
membership=None,
agreements=[
AccountAgreement(type=PRIVACY_POLICY, accept_date=date.today())
]
)
db = connect_to_db(context.client_config['DB_CONNECTION_CONFIG'])
acct_repository = AccountRepository(db)
account_id = acct_repository.add(context.account, 'foo')
context.account.id = account_id
generate_access_token(context)
generate_refresh_token(context)
@when('a monthly membership is added')
def update_membership(context):
membership_data = dict(
newMembership=True,
membershipType=MONTHLY_MEMBERSHIP,
paymentMethod=STRIPE_METHOD,
paymentToken=VISA_TOKEN
)
context.response = context.client.patch(
'/api/account',
data=json.dumps(dict(membership=membership_data)),
content_type='application/json'
)
@when('the account is requested')
def request_account(context):
db = connect_to_db(context.client_config['DB_CONNECTION_CONFIG'])
context.response_account = AccountRepository(db).get_account_by_email(
TEST_EMAIL_ADDRESS
)
@then('the account should have a monthly membership')
def monthly_account(context):
account = context.response_account
assert_that(
account.membership.type,
equal_to(MONTHLY_MEMBERSHIP)
)
assert_that(account.membership.payment_account_id, starts_with('cus'))
@given('a user with a monthly membership')
def create_monthly_account(context):
new_account_request['support'].update(
membership=MONTHLY_MEMBERSHIP,
paymentMethod=STRIPE_METHOD,
paymentToken=VISA_TOKEN
)
context.client.post(
'/api/account',
data=json.dumps(new_account_request),
content_type='application/json'
)
db = connect_to_db(context.client_config['DB_CONNECTION_CONFIG'])
account_repository = AccountRepository(db)
account = account_repository.get_account_by_email(TEST_EMAIL_ADDRESS)
context.account = account
generate_access_token(context)
generate_refresh_token(context)
@when('the membership is cancelled')
def cancel_membership(context):
membership_data = dict(
newMembership=False,
membershipType=None
)
context.client.patch(
'/api/account',
data=json.dumps(dict(membership=membership_data)),
content_type='application/json'
)
@then('the account should have no membership')
def free_account(context):
account = context.response_account
assert_that(account.membership, none())
@when('the membership is changed to yearly')
def change_to_yearly_account(context):
membership_data = dict(
newMembership=False,
membershipType=YEARLY_MEMBERSHIP
)
context.client.patch(
'/api/account',
data=json.dumps(dict(membership=membership_data)),
content_type='application/json'
)
@then('the account should have a yearly membership')
def yearly_account(context):
account = context.response_account
assert_that(account.membership.type, equal_to(YEARLY_MEMBERSHIP))
assert_that(account.membership.payment_account_id, starts_with('cus'))

View File

@ -1,19 +0,0 @@
Feature: Test the API call to update a membership
Scenario: user with free account opts into a membership
Given a user with a free account
When a monthly membership is added
And the account is requested
Then the account should have a monthly membership
Scenario: user opts out monthly membership
Given a user with a monthly membership
When the membership is cancelled
And the account is requested
Then the account should have no membership
Scenario: user changes from a monthly membership to yearly membership
Given a user with a monthly membership
When the membership is changed to yearly
And the account is requested
Then the account should have a yearly membership