added test condition for finishing on-boarding as a separate step from new account creation
parent
ca272af1a9
commit
4e5bf29eb4
|
@ -0,0 +1,22 @@
|
|||
Feature: Add a new account
|
||||
Test the API call to add an account to the database.
|
||||
|
||||
Scenario: Successful account addition
|
||||
Given a user completes new account setup
|
||||
When the new account request is submitted
|
||||
Then the request will be successful
|
||||
And the account will be added to the system
|
||||
|
||||
Scenario: Request missing a required field
|
||||
Given a user completes new account setup
|
||||
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
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
Feature: Add a new account
|
||||
Test the API call to add an account to the database.
|
||||
|
||||
Scenario: Successful account addition with membership
|
||||
Given a user completes on-boarding
|
||||
And user opts into a membership
|
||||
When the new account request is submitted
|
||||
Then the request will be successful
|
||||
And the account will be added to the system with a membership
|
||||
|
||||
Scenario: Successful account addition without membership
|
||||
Given a user completes on-boarding
|
||||
And user opts out of membership
|
||||
When the new account request is submitted
|
||||
Then the account will be added to the system without a membership
|
||||
|
||||
Scenario: Request missing a required field
|
||||
Given a user completes on-boarding
|
||||
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 account deletion with membership
|
||||
Given a user completes on-boarding
|
||||
And user opts into a membership
|
||||
When the new account request is submitted
|
||||
And the account is deleted
|
||||
Then the request will be successful
|
||||
And the membership is removed from stripe
|
|
@ -0,0 +1,141 @@
|
|||
from binascii import b2a_base64
|
||||
from datetime import date, datetime
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import call, patch
|
||||
|
||||
from behave import given, then, when
|
||||
from flask import json
|
||||
from hamcrest import assert_that, equal_to, is_in, none, 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,
|
||||
privacyPolicy=True,
|
||||
login=dict(
|
||||
federatedPlatform=None,
|
||||
federatedToken=None,
|
||||
email=b2a_base64(b'bar@mycroft.ai').decode(),
|
||||
password=b2a_base64(b'bar').decode()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@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'
|
||||
context.response = context.client.post(
|
||||
'/api/account',
|
||||
data=json.dumps(context.new_account_request),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
|
||||
@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')
|
||||
assert_that(account, not_none())
|
||||
assert_that(
|
||||
account.email_address, equal_to('bar@mycroft.ai')
|
||||
)
|
||||
|
||||
assert_that(len(account.agreements), equal_to(2))
|
||||
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())
|
|
@ -1,110 +0,0 @@
|
|||
from binascii import b2a_base64
|
||||
from datetime import date
|
||||
import os
|
||||
|
||||
import stripe
|
||||
from behave import given, then, when
|
||||
from flask import json
|
||||
from hamcrest import assert_that, equal_to, is_in, none, not_none, starts_with
|
||||
from stripe.error import InvalidRequestError
|
||||
|
||||
from selene.data.account import AccountRepository, PRIVACY_POLICY, TERMS_OF_USE
|
||||
from selene.util.db import connect_to_db
|
||||
|
||||
new_account_request = dict(
|
||||
username='barfoo',
|
||||
termsOfUse=True,
|
||||
privacyPolicy=True,
|
||||
login=dict(
|
||||
federatedPlatform=None,
|
||||
federatedToken=None,
|
||||
userEnteredEmail=b2a_base64(b'bar@mycroft.ai').decode(),
|
||||
password=b2a_base64(b'bar').decode()
|
||||
),
|
||||
support=dict(openDataset=True)
|
||||
)
|
||||
|
||||
|
||||
@given('a user completes on-boarding')
|
||||
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']['userEnteredEmail'])
|
||||
|
||||
|
||||
@when('the new account request is submitted')
|
||||
def call_add_account_endpoint(context):
|
||||
context.client.content_type = 'application/json'
|
||||
context.response = context.client.post(
|
||||
'/api/account',
|
||||
data=json.dumps(context.new_account_request),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
|
||||
@then('the account will be added to the system {membership_option}')
|
||||
def check_db_for_account(context, membership_option):
|
||||
db = connect_to_db(context.client_config['DB_CONNECTION_CONFIG'])
|
||||
acct_repository = AccountRepository(db)
|
||||
account = acct_repository.get_account_by_email('bar@mycroft.ai')
|
||||
assert_that(account, not_none())
|
||||
assert_that(
|
||||
account.email_address, equal_to('bar@mycroft.ai')
|
||||
)
|
||||
assert_that(account.username, equal_to('barfoo'))
|
||||
if membership_option == 'with a membership':
|
||||
assert_that(account.membership.type, equal_to('Monthly Membership'))
|
||||
assert_that(
|
||||
account.membership.payment_account_id,
|
||||
starts_with('cus')
|
||||
)
|
||||
elif membership_option == 'without a membership':
|
||||
assert_that(account.membership, none())
|
||||
|
||||
assert_that(len(account.agreements), equal_to(2))
|
||||
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())))
|
||||
|
||||
|
||||
@when('the account is deleted')
|
||||
def account_deleted(context):
|
||||
db = connect_to_db(context.client_config['DB_CONNECTION_CONFIG'])
|
||||
acct_repository = AccountRepository(db)
|
||||
account = acct_repository.get_account_by_email('bar@mycroft.ai')
|
||||
context.stripe_id = account.membership.payment_id
|
||||
context.response = context.client.delete('/api/account')
|
||||
|
||||
|
||||
@then('he membership is removed from stripe')
|
||||
def check_stripe(context):
|
||||
stripe_id = context.stripe_id
|
||||
assert_that(stripe_id, not_none())
|
||||
stripe.api_key = os.environ['STRIPE_PRIVATE_KEY']
|
||||
subscription_not_found = False
|
||||
try:
|
||||
stripe.Subscription.retrieve(stripe_id)
|
||||
except InvalidRequestError:
|
||||
subscription_not_found = True
|
||||
assert_that(subscription_not_found, equal_to(True))
|
|
@ -286,8 +286,8 @@ class AccountEndpoint(SeleneEndpoint):
|
|||
self._add_membership(membership_change, active_membership)
|
||||
|
||||
def _get_active_membership(self):
|
||||
membership_repo = MembershipRepository(self.db)
|
||||
active_membership = membership_repo.get_active_account_membership(
|
||||
acct_repository = AccountRepository(self.db)
|
||||
active_membership = acct_repository.get_active_account_membership(
|
||||
self.account.id
|
||||
)
|
||||
|
||||
|
@ -325,8 +325,8 @@ class AccountEndpoint(SeleneEndpoint):
|
|||
def _cancel_membership(self, active_membership):
|
||||
cancel_stripe_subscription(active_membership.payment_id)
|
||||
active_membership.end_date = datetime.utcnow()
|
||||
membership_repository = MembershipRepository(self.db)
|
||||
membership_repository.finish_membership(active_membership)
|
||||
account_repository = AccountRepository(self.db)
|
||||
account_repository.end_membership(active_membership)
|
||||
|
||||
def _update_username(self, username):
|
||||
self.account_repository.update_username(self.account.id, username)
|
||||
|
|
|
@ -63,20 +63,6 @@ class AccountRepository(RepositoryBase):
|
|||
)
|
||||
self.cursor.insert(request)
|
||||
|
||||
def add_membership(self, acct_id: str, membership: AccountMembership):
|
||||
"""A membership is optional, add it if one was selected"""
|
||||
request = self._build_db_request(
|
||||
sql_file_name='add_account_membership.sql',
|
||||
args=dict(
|
||||
account_id=acct_id,
|
||||
membership_type=membership.type,
|
||||
payment_method=membership.payment_method,
|
||||
payment_account_id=membership.payment_account_id,
|
||||
payment_id=membership.payment_id
|
||||
)
|
||||
)
|
||||
self.cursor.insert(request)
|
||||
|
||||
def remove(self, account: Account):
|
||||
"""Delete and account and all of its children"""
|
||||
request = self._build_db_request(
|
||||
|
@ -278,3 +264,42 @@ class AccountRepository(RepositoryBase):
|
|||
'thirtyDaysMinus': report_30_days['paid_minus']
|
||||
}]
|
||||
return report_table
|
||||
|
||||
def add_membership(self, acct_id: str, membership: AccountMembership):
|
||||
"""A membership is optional, add it if one was selected"""
|
||||
request = self._build_db_request(
|
||||
sql_file_name='add_account_membership.sql',
|
||||
args=dict(
|
||||
account_id=acct_id,
|
||||
membership_type=membership.type,
|
||||
payment_method=membership.payment_method,
|
||||
payment_account_id=membership.payment_account_id,
|
||||
payment_id=membership.payment_id
|
||||
)
|
||||
)
|
||||
self.cursor.insert(request)
|
||||
|
||||
def end_membership(self, membership: AccountMembership):
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='end_membership.sql',
|
||||
args=dict(
|
||||
id=membership.id,
|
||||
membership_ts_range='[{start},{end}]'.format(
|
||||
start=membership.start_date,
|
||||
end=membership.end_date
|
||||
)
|
||||
)
|
||||
)
|
||||
self.cursor.update(db_request)
|
||||
|
||||
def get_active_account_membership(self, account_id) -> AccountMembership:
|
||||
account_membership = None
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='get_active_membership_by_account_id.sql',
|
||||
args=dict(account_id=account_id)
|
||||
)
|
||||
db_result = self.cursor.select_one(db_request)
|
||||
if db_result:
|
||||
account_membership = AccountMembership(**db_result)
|
||||
|
||||
return account_membership
|
||||
|
|
|
@ -26,18 +26,6 @@ class MembershipRepository(RepositoryBase):
|
|||
db_result = self.cursor.select_one(db_request)
|
||||
return Membership(**db_result)
|
||||
|
||||
def get_active_account_membership(self, account_id) -> AccountMembership:
|
||||
account_membership = None
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='get_active_membership_by_account_id.sql',
|
||||
args=dict(account_id=account_id)
|
||||
)
|
||||
db_result = self.cursor.select_one(db_request)
|
||||
if db_result:
|
||||
account_membership = AccountMembership(**db_result)
|
||||
|
||||
return account_membership
|
||||
|
||||
def add(self, membership: Membership):
|
||||
db_request = self._build_db_request(
|
||||
'add_membership.sql',
|
||||
|
@ -57,16 +45,3 @@ class MembershipRepository(RepositoryBase):
|
|||
args=dict(membership_id=membership.id)
|
||||
)
|
||||
self.cursor.delete(db_request)
|
||||
|
||||
def finish_membership(self, membership: AccountMembership):
|
||||
db_request = self._build_db_request(
|
||||
sql_file_name='finish_membership.sql',
|
||||
args=dict(
|
||||
id=membership.id,
|
||||
membership_ts_range='[{start},{end}]'.format(
|
||||
start=membership.start_date,
|
||||
end=membership.end_date
|
||||
)
|
||||
)
|
||||
)
|
||||
self.cursor.update(db_request)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
SELECT
|
||||
acc_mem.id,
|
||||
mem.type,
|
||||
LOWER(acc_mem.membership_ts_range) start_date,
|
||||
LOWER(acc_mem.membership_ts_range)::date start_date,
|
||||
acc_mem.payment_method,
|
||||
payment_account_id,
|
||||
payment_id
|
||||
|
|
Loading…
Reference in New Issue