Commit 0135e93c by Clinton Blackburn

Fixed checkout acceptance tests

ECOM-7099
parent b10116a8
import requests from edx_rest_api_client.client import EdxRestApiClient
from requests.auth import AuthBase from requests.auth import AuthBase
from e2e.config import ACCESS_TOKEN, ENROLLMENT_API_URL from e2e.config import ENROLLMENT_API_URL, OAUTH_ACCESS_TOKEN_URL, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET
def get_access_token():
""" Returns an access token and expiration date from the OAuth provider.
Returns:
(str, datetime)
"""
# TODO Use JWT auth once https://github.com/edx/edx-platform/pull/14577 is merged/released.
return EdxRestApiClient.get_oauth_access_token(
OAUTH_ACCESS_TOKEN_URL, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET,
)
class BearerAuth(AuthBase): class BearerAuth(AuthBase):
...@@ -18,13 +31,13 @@ class BearerAuth(AuthBase): ...@@ -18,13 +31,13 @@ class BearerAuth(AuthBase):
class EnrollmentApiClient(object): class EnrollmentApiClient(object):
def __init__(self, host=None, key=None): def __init__(self):
self.host = host or ENROLLMENT_API_URL access_token, __ = get_access_token()
self.key = key or ACCESS_TOKEN self.client = EdxRestApiClient(ENROLLMENT_API_URL, oauth_access_token=access_token, append_slash=False)
def get_enrollment_status(self, username, course_id): def get_enrollment_status(self, username, course_id):
""" """
Retrieve the enrollment status for given user in a given course. Retrieve the enrollment status for given user in a given course.
""" """
url = '{host}/enrollment/{username},{course_id}'.format(host=self.host, username=username, course_id=course_id) param = '{username},{course_id}'.format(username=username, course_id=course_id)
return requests.get(url, auth=BearerAuth(self.key)).json() return self.client.enrollment(param).get()
...@@ -2,9 +2,12 @@ import os ...@@ -2,9 +2,12 @@ import os
from e2e.utils import str2bool from e2e.utils import str2bool
ACCESS_TOKEN = os.environ.get('ACCESS_TOKEN') OAUTH_ACCESS_TOKEN_URL = os.environ.get('OAUTH_ACCESS_TOKEN_URL')
if ACCESS_TOKEN is None: OAUTH_CLIENT_ID = os.environ.get('OAUTH_CLIENT_ID')
raise RuntimeError('A valid OAuth2 access token is required.') OAUTH_CLIENT_SECRET = os.environ.get('OAUTH_CLIENT_SECRET')
if not all([OAUTH_ACCESS_TOKEN_URL, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET]):
raise RuntimeError('Valid OAuth details must be provided.')
HONOR_COURSE_ID = os.environ.get('HONOR_COURSE_ID') HONOR_COURSE_ID = os.environ.get('HONOR_COURSE_ID')
VERIFIED_COURSE_ID = os.environ.get('VERIFIED_COURSE_ID') VERIFIED_COURSE_ID = os.environ.get('VERIFIED_COURSE_ID')
...@@ -20,9 +23,11 @@ except AttributeError: ...@@ -20,9 +23,11 @@ except AttributeError:
ECOMMERCE_API_URL = os.environ.get('ECOMMERCE_API_URL', ECOMMERCE_URL_ROOT + '/api/v2') ECOMMERCE_API_URL = os.environ.get('ECOMMERCE_API_URL', ECOMMERCE_URL_ROOT + '/api/v2')
MAX_COMPLETION_RETRIES = int(os.environ.get('MAX_COMPLETION_RETRIES', 3)) MAX_COMPLETION_RETRIES = int(os.environ.get('MAX_COMPLETION_RETRIES', 3))
PAYPAL_EMAIL = os.environ.get('PAYPAL_EMAIL') PAYPAL_EMAIL = os.environ.get('PAYPAL_EMAIL')
PAYPAL_PASSWORD = os.environ.get('PAYPAL_PASSWORD') PAYPAL_PASSWORD = os.environ.get('PAYPAL_PASSWORD')
ENABLE_CYBERSOURCE_TESTS = str2bool(os.environ.get('ENABLE_CYBERSOURCE_TESTS', True)) if not all([PAYPAL_EMAIL, PAYPAL_PASSWORD]):
raise RuntimeError('PayPal credentials are required to fully test payment.')
try: try:
MARKETING_URL_ROOT = os.environ.get('MARKETING_URL_ROOT').strip('/') MARKETING_URL_ROOT = os.environ.get('MARKETING_URL_ROOT').strip('/')
......
...@@ -2,8 +2,7 @@ from datetime import date ...@@ -2,8 +2,7 @@ from datetime import date
CODE = 'ABCD' CODE = 'ABCD'
# cybersource data ADDRESS_US = {
CYBERSOURCE_DATA1 = {
'country': 'US', 'country': 'US',
'state': 'MA', 'state': 'MA',
'line1': '141 Portland Ave.', 'line1': '141 Portland Ave.',
...@@ -11,7 +10,7 @@ CYBERSOURCE_DATA1 = { ...@@ -11,7 +10,7 @@ CYBERSOURCE_DATA1 = {
'city': 'Cambridge', 'city': 'Cambridge',
'postal_code': '02141', 'postal_code': '02141',
} }
CYBERSOURCE_DATA2 = { ADDRESS_FR = {
'country': 'FR', 'country': 'FR',
'state': None, 'state': None,
'line1': 'Champ de Mars', 'line1': 'Champ de Mars',
......
import logging import logging
import time
import uuid import uuid
import requests import requests
...@@ -9,22 +10,11 @@ from selenium.webdriver.support import expected_conditions as EC ...@@ -9,22 +10,11 @@ from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.select import Select from selenium.webdriver.support.select import Select
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from e2e.api import EnrollmentApiClient from e2e.api import EnrollmentApiClient, get_access_token
from e2e.config import ( from e2e.config import (
LMS_AUTO_AUTH, LMS_AUTO_AUTH, ECOMMERCE_URL_ROOT, LMS_PASSWORD, LMS_EMAIL, LMS_URL_ROOT, BASIC_AUTH_USERNAME, BASIC_AUTH_PASSWORD,
ECOMMERCE_URL_ROOT, ECOMMERCE_API_URL, LMS_USERNAME, MAX_COMPLETION_RETRIES, PAYPAL_PASSWORD, PAYPAL_EMAIL, LMS_HTTPS,
LMS_PASSWORD, MARKETING_URL_ROOT
LMS_EMAIL,
LMS_URL_ROOT,
BASIC_AUTH_USERNAME,
BASIC_AUTH_PASSWORD,
ECOMMERCE_API_URL,
LMS_USERNAME,
ACCESS_TOKEN,
MAX_COMPLETION_RETRIES,
PAYPAL_PASSWORD,
PAYPAL_EMAIL,
LMS_HTTPS
) )
from e2e.expected_conditions import input_provided from e2e.expected_conditions import input_provided
from e2e.pages import submit_lms_login_form from e2e.pages import submit_lms_login_form
...@@ -98,7 +88,8 @@ class LMSLogoutMixin(object): ...@@ -98,7 +88,8 @@ class LMSLogoutMixin(object):
self.lms_logout_page = LMSLogoutPage(self.browser) self.lms_logout_page = LMSLogoutPage(self.browser)
def assert_on_lms_logout_page(self): def assert_on_lms_logout_page(self):
self.assertTrue(self.lms_logout_page.is_browser_on_page()) lms_homepage_url = MARKETING_URL_ROOT or LMS_URL_ROOT
self.assertTrue(self.browser.current_url.startswith(lms_homepage_url))
def logout_via_lms(self): def logout_via_lms(self):
self.lms_logout_page.visit() self.lms_logout_page.visit()
...@@ -163,7 +154,8 @@ class EnrollmentApiMixin(object): ...@@ -163,7 +154,8 @@ class EnrollmentApiMixin(object):
class EcommerceApiMixin(object): class EcommerceApiMixin(object):
@property @property
def ecommerce_api_client(self): def ecommerce_api_client(self):
return EdxRestApiClient(ECOMMERCE_API_URL, oauth_access_token=ACCESS_TOKEN) access_token, __ = get_access_token()
return EdxRestApiClient(ECOMMERCE_API_URL, oauth_access_token=access_token)
def assert_order_created_and_completed(self): def assert_order_created_and_completed(self):
orders = self.ecommerce_api_client.orders.get()['results'] orders = self.ecommerce_api_client.orders.get()['results']
...@@ -223,20 +215,28 @@ class PaymentMixin(object): ...@@ -223,20 +215,28 @@ class PaymentMixin(object):
except TimeoutException: except TimeoutException:
pass pass
def wait_for_payment_form(self):
""" Wait for the payment form to load. """
wait = WebDriverWait(self.browser, 10)
wait.until(EC.presence_of_element_located((By.ID, 'paymentForm')))
def checkout_with_paypal(self): def checkout_with_paypal(self):
""" Completes the checkout process via PayPal. """ """ Completes the checkout process via PayPal. """
self.wait_for_payment_form()
# Wait for the button handler to be setup
time.sleep(0.5)
# Click the payment button # Click the payment button
self.browser.find_element_by_css_selector('#paypal').click() self.browser.find_element_by_css_selector('button.payment-button[data-processor-name=paypal]').click()
# Wait for login form to load. PayPal's test environment is slow. # Wait for login form to load. PayPal's test environment is slow.
wait = WebDriverWait(self.browser, 30) wait = WebDriverWait(self.browser, 30)
iframe_present = EC.presence_of_element_located((By.CSS_SELECTOR, '#injectedUnifiedLogin > iframe')) login_iframe = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '#injectedUnifiedLogin > iframe')))
iframe = wait.until(iframe_present) # TODO Determine how to get past the TypeError here.
self.browser.switch_to.frame(login_iframe)
# Log into PayPal # Log into PayPal
self.browser.switch_to.frame(iframe)
email = self.browser.find_element_by_css_selector('input#email') email = self.browser.find_element_by_css_selector('input#email')
password = self.browser.find_element_by_css_selector('input#password') password = self.browser.find_element_by_css_selector('input#password')
...@@ -269,28 +269,16 @@ class PaymentMixin(object): ...@@ -269,28 +269,16 @@ class PaymentMixin(object):
confirmation_button.click() confirmation_button.click()
def checkout_with_cybersource(self, address): def checkout_with_credit_card(self, address):
""" Completes the checkout process via CyberSource. """ """ Submit the payment form. """
self.wait_for_payment_form()
# Click the payment button
self.browser.find_element_by_css_selector('#cybersource').click()
self.dismiss_alert()
# Wait for form to load
wait = WebDriverWait(self.browser, 10)
billing_details_present = EC.presence_of_element_located((By.ID, 'billing_details'))
wait.until(billing_details_present)
# Select the credit card type (Visa) first since it triggers the display of additional fields
self.browser.find_element_by_css_selector('#card_type_001').click() # Visa
# Select the appropriate <option> elements # Select the appropriate <option> elements
select_fields = ( select_fields = (
('#bill_to_address_country', address['country']), ('#id_country', address['country']),
('#bill_to_address_state_us_ca', address['state']), ('#id_state', address['state']),
('#card_expiry_month', '01'), ('#card-expiry-month', '12'),
('#card_expiry_year', '2020') ('#card-expiry-year', '2030')
) )
for selector, value in select_fields: for selector, value in select_fields:
if value: if value:
...@@ -299,24 +287,21 @@ class PaymentMixin(object): ...@@ -299,24 +287,21 @@ class PaymentMixin(object):
# Fill in the text fields # Fill in the text fields
billing_information = { billing_information = {
'bill_to_forename': 'Ed', 'id_first_name': 'Ed',
'bill_to_surname': 'Xavier', 'id_last_name': 'Xavier',
'bill_to_address_line1': address['line1'], 'id_address_line1': address['line1'],
'bill_to_address_line2': address['line2'], 'id_address_line2': address['line2'],
'bill_to_address_city': address['city'], 'id_city': address['city'],
'bill_to_address_postal_code': address['postal_code'], 'id_postal_code': address['postal_code'],
'bill_to_email': 'edx@example.com', 'card-number': '4111111111111111',
'card_number': '4111111111111111', 'card-cvn': '123'
'card_cvn': '1234'
} }
for field, value in billing_information.items(): for field, value in billing_information.items():
self.browser.find_element_by_css_selector('#' + field).send_keys(value) self.browser.find_element_by_css_selector('#' + field).send_keys(value)
# Click the payment button # Click the payment button
self.browser.find_element_by_css_selector('input[type=submit]').click() self.browser.find_element_by_css_selector('#payment-button').click()
self.dismiss_alert()
def assert_receipt_page_loads(self): def assert_receipt_page_loads(self):
""" Verifies the receipt page loaded in the browser. """ """ Verifies the receipt page loaded in the browser. """
......
# Packages required to run e2e tests # Packages required to run e2e tests
bok-choy==0.5.0 bok-choy==0.6.2
ddt==1.0.1 ddt==1.1.1
django-nose==1.4.2 django-nose==1.4.4
edx-rest-api-client==1.5.0 edx-rest-api-client==1.7.1
nose-ignore-docstring==0.2 nose-ignore-docstring==0.2
requests==2.9.1 selenium>=3.0.2
selenium>=2.53.1
from unittest import skipUnless
import ddt import ddt
from bok_choy.web_app_test import WebAppTest from bok_choy.web_app_test import WebAppTest
from e2e.config import VERIFIED_COURSE_ID, ENABLE_CYBERSOURCE_TESTS from e2e.config import VERIFIED_COURSE_ID
from e2e.constants import CODE, CYBERSOURCE_DATA1, CYBERSOURCE_DATA2 from e2e.constants import CODE, ADDRESS_US, ADDRESS_FR
from e2e.mixins import (CouponMixin, EcommerceApiMixin, EnrollmentApiMixin, from e2e.mixins import (CouponMixin, EcommerceApiMixin, EnrollmentApiMixin, LogistrationMixin, UnenrollmentMixin,
LogistrationMixin, UnenrollmentMixin, PaymentMixin) PaymentMixin)
from e2e.pages.basket import BasketPage from e2e.pages.basket import BasketPage
from e2e.pages.coupons import CouponsCreatePage, CouponsDetailsPage, CouponsListPage, RedeemVoucherPage from e2e.pages.coupons import CouponsCreatePage, CouponsDetailsPage, CouponsListPage, RedeemVoucherPage
from e2e.pages.ecommerce import EcommerceDashboardHomePage from e2e.pages.ecommerce import EcommerceDashboardHomePage
...@@ -69,8 +67,7 @@ class CouponCheckoutTests(CouponMixin, UnenrollmentMixin, EcommerceApiMixin, Enr ...@@ -69,8 +67,7 @@ class CouponCheckoutTests(CouponMixin, UnenrollmentMixin, EcommerceApiMixin, Enr
self.assert_order_created_and_completed() self.assert_order_created_and_completed()
self.assert_user_enrolled(self.username, self.course_id, 'verified') self.assert_user_enrolled(self.username, self.course_id, 'verified')
@skipUnless(ENABLE_CYBERSOURCE_TESTS, 'CyberSource tests are not enabled.') @ddt.data(ADDRESS_US, ADDRESS_FR)
@ddt.data(CYBERSOURCE_DATA1, CYBERSOURCE_DATA2)
def test_discount_checkout_with_cybersource(self, address): def test_discount_checkout_with_cybersource(self, address):
""" Test redemption of discount code and purchase of course via Cybersource """ """ Test redemption of discount code and purchase of course via Cybersource """
self.start_redeem_flow(is_discount=True) self.start_redeem_flow(is_discount=True)
......
from abc import ABCMeta
from unittest import skip
from unittest import skipUnless from unittest import skipUnless
import ddt import ddt
...@@ -6,20 +8,28 @@ from selenium.webdriver.common.by import By ...@@ -6,20 +8,28 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from e2e.config import (VERIFIED_COURSE_ID, MARKETING_URL_ROOT, from e2e.config import (VERIFIED_COURSE_ID, MARKETING_URL_ROOT, PAYPAL_PASSWORD, PAYPAL_EMAIL, BULK_PURCHASE_SKU)
PAYPAL_PASSWORD, PAYPAL_EMAIL, ENABLE_CYBERSOURCE_TESTS, from e2e.constants import ADDRESS_US, ADDRESS_FR
BULK_PURCHASE_SKU)
from e2e.constants import CYBERSOURCE_DATA1, CYBERSOURCE_DATA2
from e2e.mixins import (LogistrationMixin, EnrollmentApiMixin, EcommerceApiMixin, from e2e.mixins import (LogistrationMixin, EnrollmentApiMixin, EcommerceApiMixin,
PaymentMixin, UnenrollmentMixin) PaymentMixin, UnenrollmentMixin)
from e2e.pages.basket import BasketAddProductPage
from e2e.pages.lms import LMSCourseModePage from e2e.pages.lms import LMSCourseModePage
from e2e.pages.marketing import MarketingCourseAboutPage from e2e.pages.marketing import MarketingCourseAboutPage
from e2e.pages.basket import BasketAddProductPage
class BasePaymentTest(UnenrollmentMixin, EcommerceApiMixin, EnrollmentApiMixin, LogistrationMixin, PaymentMixin,
WebAppTest):
__metaclass__ = ABCMeta
def setUp(self):
super(BasePaymentTest, self).setUp()
self.course_id = VERIFIED_COURSE_ID
self.username, self.password, self.email = self.get_lms_user()
self.basket_add_product_page = BasketAddProductPage(self.browser)
@ddt.ddt @ddt.ddt
class VerifiedCertificatePaymentTests(UnenrollmentMixin, EcommerceApiMixin, EnrollmentApiMixin, LogistrationMixin, class VerifiedCertificatePaymentTests(BasePaymentTest):
PaymentMixin, WebAppTest):
def setUp(self): def setUp(self):
super(VerifiedCertificatePaymentTests, self).setUp() super(VerifiedCertificatePaymentTests, self).setUp()
self.course_id = VERIFIED_COURSE_ID self.course_id = VERIFIED_COURSE_ID
...@@ -45,10 +55,37 @@ class VerifiedCertificatePaymentTests(UnenrollmentMixin, EcommerceApiMixin, Enro ...@@ -45,10 +55,37 @@ class VerifiedCertificatePaymentTests(UnenrollmentMixin, EcommerceApiMixin, Enro
course_modes_page = LMSCourseModePage(self.browser, self.course_id) course_modes_page = LMSCourseModePage(self.browser, self.course_id)
course_modes_page.visit() course_modes_page.visit()
# Click the purchase button on the track selection page to take # Click the purchase button on the track selection page to take the browser to the payment selection page.
# the browser to the payment selection page.
self.browser.find_element_by_css_selector('input[name=verified_mode]').click() self.browser.find_element_by_css_selector('input[name=verified_mode]').click()
@ddt.data(ADDRESS_US, ADDRESS_FR)
def test_checkout_with_credit_card(self, address):
""" Test the client-side checkout page.
We use a U.S. address and a French address since the checkout page requires a state for the U.S. and
Canada, but not for other countries.
"""
self._start_checkout()
self.checkout_with_credit_card(address)
self.assert_receipt_page_loads()
self.assert_order_created_and_completed()
self.assert_user_enrolled(self.username, self.course_id, 'verified')
@skip('See ECOM-7298.')
def test_paypal(self):
""" Test checkout with PayPal. """
self._start_checkout()
self.checkout_with_paypal()
self.assert_receipt_page_loads()
self.assert_order_created_and_completed()
self.assert_user_enrolled(self.username, self.course_id, 'verified')
@ddt.ddt
@skipUnless(BULK_PURCHASE_SKU, 'A bulk purchase SKU must be provided to run bulk purchase tests!')
class BulkSeatPaymentTests(BasePaymentTest):
def _start_bulk_seat_checkout(self): def _start_bulk_seat_checkout(self):
""" Begin the checkout process for a verified certificate. """ """ Begin the checkout process for a verified certificate. """
self.login_with_lms(self.email, self.password) self.login_with_lms(self.email, self.password)
...@@ -61,43 +98,16 @@ class VerifiedCertificatePaymentTests(UnenrollmentMixin, EcommerceApiMixin, Enro ...@@ -61,43 +98,16 @@ class VerifiedCertificatePaymentTests(UnenrollmentMixin, EcommerceApiMixin, Enro
# the browser to the payment selection page. # the browser to the payment selection page.
self.browser.find_element_by_css_selector('button[id=paypal]').click() self.browser.find_element_by_css_selector('button[id=paypal]').click()
@skipUnless(ENABLE_CYBERSOURCE_TESTS, 'CyberSource tests are not enabled.') def test_bulk_seat_purchase_with_credit_card(self):
@ddt.data(CYBERSOURCE_DATA1, CYBERSOURCE_DATA2)
def test_cybersource(self, address):
""" Test checkout with CyberSource. """
self._start_checkout()
self.checkout_with_cybersource(address)
self.assert_receipt_page_loads()
self.assert_order_created_and_completed()
self.assert_user_enrolled(self.username, self.course_id, 'verified')
@skipUnless(ENABLE_CYBERSOURCE_TESTS and BULK_PURCHASE_SKU,
'CyberSource tests are not enabled, or Bulk Purchase SKU not provided, skipping Bulk Purchase tests.')
@ddt.data(CYBERSOURCE_DATA1, CYBERSOURCE_DATA2)
def test_bulk_seat_purchase_cybersource(self, address):
""" Test bulk seat purchase checkout with CyberSource. """ """ Test bulk seat purchase checkout with CyberSource. """
self._start_bulk_seat_checkout() self._start_bulk_seat_checkout()
self.browser.find_element_by_css_selector('button[id=cybersource]').click() self.browser.find_element_by_css_selector('button[id=cybersource]').click()
self.checkout_with_cybersource(address) self.checkout_with_credit_card(ADDRESS_US)
self.assert_receipt_page_loads() self.assert_receipt_page_loads()
self.assert_user_not_enrolled(self.username, self.course_id) self.assert_user_not_enrolled(self.username, self.course_id)
def test_paypal(self): def test_bulk_seat_purchase_with_paypal(self):
""" Test checkout with PayPal. """
if not (PAYPAL_EMAIL and PAYPAL_PASSWORD):
self.fail('No PayPal credentials supplied!')
self._start_checkout()
self.checkout_with_paypal()
self.assert_receipt_page_loads()
self.assert_order_created_and_completed()
self.assert_user_enrolled(self.username, self.course_id, 'verified')
@skipUnless(BULK_PURCHASE_SKU, 'Bulk Purchase SKU not provided, skipping Bulk Purchase tests.')
def test_bulk_seat_purchase_paypal(self):
""" Test bulk seat purchase checkout with PayPal. """ """ Test bulk seat purchase checkout with PayPal. """
if not (PAYPAL_EMAIL and PAYPAL_PASSWORD): if not (PAYPAL_EMAIL and PAYPAL_PASSWORD):
self.fail('No PayPal credentials supplied!') self.fail('No PayPal credentials supplied!')
......
# Packages required for testing # Packages required for testing
-r base.txt -r base.txt
bok-choy==0.4.7 bok-choy==0.6.2
coverage==4.2 coverage==4.3.4
ddt==1.0.0 ddt==1.1.1
django-nose==1.4.2 django-nose==1.4.4
factory-boy==2.8.1 factory-boy==2.8.1
freezegun==0.3.7 freezegun==0.3.7
httpretty==0.8.14 httpretty==0.8.14
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment