Bok Choy tests for the split payment and verification flow

Adds mode creation endpoint to course_modes app and ability to bypass media device access prompt when proceeding through verification flow in a testing environment.
This commit is contained in:
Renzo Lucioni
2015-01-09 09:44:26 -05:00
parent 27fee62039
commit c6777a5b25
15 changed files with 672 additions and 17 deletions

View File

@@ -0,0 +1,71 @@
"""Mode creation page (used to add modes to courses during testing)."""
import re
import urllib
from bok_choy.page_object import PageObject
from . import BASE_URL
class ModeCreationPage(PageObject):
"""The mode creation page.
When allowed by the Django settings file, visiting this page allows modes to be
created for an existing course.
"""
def __init__(self, browser, course_id, mode_slug=None, mode_display_name=None, min_price=None, suggested_prices=None, currency=None):
"""The mode creation page is an endpoint for HTTP GET requests.
By default, it will create an 'honor' mode for the given course with display name
'Honor Code', a minimum price of 0, no suggested prices, and using USD as the currency.
Arguments:
browser (Browser): The browser instance.
course_id (unicode): The ID of the course for which modes are to be created.
Keyword Arguments:
mode_slug (str): The mode to add, either 'honor', 'verified', or 'professional'
mode_display_name (str): Describes the new course mode
min_price (int): The minimum price a user must pay to enroll in the new course mode
suggested_prices (str): Comma-separated prices to suggest to the user.
currency (str): The currency in which to list prices.
"""
super(ModeCreationPage, self).__init__(browser)
self.course_id = course_id
self._parameters = {}
if mode_slug is not None:
self._parameters['mode_slug'] = mode_slug
if mode_display_name is not None:
self._parameters['mode_display_name'] = mode_display_name
if min_price is not None:
self._parameters['min_price'] = min_price
if suggested_prices is not None:
self._parameters['suggested_prices'] = suggested_prices
if currency is not None:
self._parameters['currency'] = currency
@property
def url(self):
"""Construct the mode creation URL."""
url = '{base}/course_modes/create_mode/{course_id}'.format(
base=BASE_URL,
course_id=self.course_id
)
query_string = urllib.urlencode(self._parameters)
if query_string:
url += '?' + query_string
return url
def is_browser_on_page(self):
message = self.q(css='BODY').text[0]
match = re.search(r'Mode ([^$]+) created for course with ID ([^$]+).$', message)
return True if match else False

View File

@@ -13,8 +13,32 @@ class DashboardPage(PageObject):
Student dashboard, where the student can view
courses she/he has registered for.
"""
def __init__(self, browser, separate_verified=False):
"""Initialize the page.
url = BASE_URL + "/dashboard"
Arguments:
browser (Browser): The browser instance.
Keyword Arguments:
separate_verified (Boolean): Whether to use the split payment and
verification flow.
"""
super(DashboardPage, self).__init__(browser)
if separate_verified:
self._querystring = "?separate-verified=1"
else:
self._querystring = "?disable-separate-verified=1"
@property
def url(self):
"""Return the URL corresponding to the dashboard."""
url = "{base}/dashboard{querystring}".format(
base=BASE_URL,
querystring=self._querystring
)
return url
def is_browser_on_page(self):
return self.q(css='section.my-courses').present
@@ -44,6 +68,60 @@ class DashboardPage(PageObject):
return self.q(css='section.info > hgroup > h3 > a').map(_get_course_name).results
def get_enrollment_mode(self, course_name):
"""Get the enrollment mode for a given course on the dashboard.
Arguments:
course_name (str): The name of the course whose mode should be retrieved.
Returns:
String, indicating the enrollment mode for the course corresponding to
the provided course name.
Raises:
Exception, if no course with the provided name is found on the dashboard.
"""
# Filter elements by course name, only returning the relevant course item
course_listing = self.q(css=".course").filter(lambda el: course_name in el.text).results
if course_listing:
# There should only be one course listing for the provided course name.
# Since 'ENABLE_VERIFIED_CERTIFICATES' is true in the Bok Choy settings, we
# can expect two classes to be present on <article> elements, one being 'course'
# and the other being the enrollment mode.
enrollment_mode = course_listing[0].get_attribute('class').split('course ')[1]
else:
raise Exception("No course named {} was found on the dashboard".format(course_name))
return enrollment_mode
def upgrade_enrollment(self, course_name, upgrade_page):
"""Interact with the upgrade button for the course with the provided name.
Arguments:
course_name (str): The name of the course whose mode should be checked.
upgrade_page (PageObject): The page to wait on after clicking the upgrade button. Importing
the definition of PaymentAndVerificationFlow results in a circular dependency.
Raises:
Exception, if no enrollment corresponding to the provided course name appears
on the dashboard.
"""
# Filter elements by course name, only returning the relevant course item
course_listing = self.q(css=".course").filter(lambda el: course_name in el.text).results
if course_listing:
# There should only be one course listing corresponding to the provided course name.
el = course_listing[0]
# Expand the upsell copy and click the upgrade button
el.find_element_by_css_selector('.message-upsell').click()
el.find_element_by_css_selector('#upgrade-to-verified').click()
upgrade_page.wait_for_page()
else:
raise Exception("No enrollment for {} is visible on the dashboard.".format(course_name))
def view_course(self, course_id):
"""
Go to the course with `course_id` (e.g. edx/Open_DemoX/edx_demo_course)

View File

@@ -0,0 +1,168 @@
"""Payment and verification pages"""
import re
from urllib import urlencode
from bok_choy.page_object import PageObject, unguarded
from bok_choy.promise import Promise, EmptyPromise
from . import BASE_URL
from .dashboard import DashboardPage
class PaymentAndVerificationFlow(PageObject):
"""Interact with the split payment and verification flow.
These pages are currently hidden behind the feature flag
`SEPARATE_VERIFICATION_FROM_PAYMENT`, which is enabled in
the Bok Choy settings.
When enabled, the flow can be accessed at the following URLs:
`/verify_student/start-flow/{course}/`
`/verify_student/upgrade/{course}/`
`/verify_student/verify-now/{course}/`
`/verify_student/verify-later/{course}/`
`/verify_student/payment-confirmation/{course}/`
Users can reach the flow when attempting to enroll in a course's verified
mode, either directly from the track selection page, or by upgrading from
the honor mode. Users can also reach the flow when attempting to complete
a deferred verification, or when attempting to view a receipt corresponding
to an earlier payment.
"""
def __init__(self, browser, course_id, entry_point='start-flow'):
"""Initialize the page.
Arguments:
browser (Browser): The browser instance.
course_id (unicode): The course in which the user is enrolling.
Keyword Arguments:
entry_point (str): Where to begin the flow; must be one of 'start-flow',
'upgrade', 'verify-now', verify-later', or 'payment-confirmation'.
Raises:
ValueError
"""
super(PaymentAndVerificationFlow, self).__init__(browser)
self._course_id = course_id
if entry_point not in ['start-flow', 'upgrade', 'verify-now', 'verify-later', 'payment-confirmation']:
raise ValueError(
"Entry point must be either 'start-flow', 'upgrade', 'verify-now', 'verify-later', or 'payment-confirmation'."
)
self._entry_point = entry_point
@property
def url(self):
"""Return the URL corresponding to the initial position in the flow."""
url = "{base}/verify_student/{entry_point}/{course}".format(
base=BASE_URL,
entry_point=self._entry_point,
course=self._course_id
)
return url
def is_browser_on_page(self):
"""Check if a step in the payment and verification flow has loaded."""
return (
self.q(css="div .make-payment-step").is_present() or
self.q(css="div .payment-confirmation-step").is_present() or
self.q(css="div .face-photo-step").is_present() or
self.q(css="div .id-photo-step").is_present() or
self.q(css="div .review-photos-step").is_present() or
self.q(css="div .enrollment-confirmation-step").is_present()
)
def indicate_contribution(self):
"""Interact with the radio buttons appearing on the first page of the upgrade flow."""
self.q(css=".contribution-option > input").first.click()
def proceed_to_payment(self):
"""Interact with the payment button."""
self.q(css="#pay_button").click()
FakePaymentPage(self.browser, self._course_id).wait_for_page()
def immediate_verification(self):
"""Interact with the immediate verification button."""
self.q(css="#verify_now_button").click()
PaymentAndVerificationFlow(self.browser, self._course_id, entry_point='verify-now').wait_for_page()
def defer_verification(self):
"""Interact with the link allowing the user to defer their verification."""
self.q(css="#verify_later_button").click()
DashboardPage(self.browser).wait_for_page()
def webcam_capture(self):
"""Interact with a webcam capture button."""
self.q(css="#webcam_capture_button").click()
def _check_func():
next_step_button_classes = self.q(css="#next_step_button").attrs('class')
next_step_button_enabled = 'is-disabled' not in next_step_button_classes
return (next_step_button_enabled, next_step_button_classes)
# Check that the #next_step_button is enabled before returning control to the caller
Promise(_check_func, "The 'Next Step' button is enabled.").fulfill()
def next_verification_step(self, next_page_object):
"""Interact with the 'Next' step button found in the verification flow."""
self.q(css="#next_step_button").click()
next_page_object.wait_for_page()
def go_to_dashboard(self):
"""Interact with the link to the dashboard appearing on the enrollment confirmation page."""
if self.q(css="div .enrollment-confirmation-step").is_present():
self.q(css=".action-primary").click()
else:
raise Exception("The dashboard can only be accessed from the enrollment confirmation.")
DashboardPage(self.browser, separate_verified=True).wait_for_page()
class FakePaymentPage(PageObject):
"""Interact with the fake payment endpoint.
This page is hidden behind the feature flag `ENABLE_PAYMENT_FAKE`,
which is enabled in the Bok Choy env settings.
Configuring this payment endpoint also requires configuring the Bok Choy
auth settings with the following:
"CC_PROCESSOR_NAME": "CyberSource2",
"CC_PROCESSOR": {
"CyberSource2": {
"SECRET_KEY": <string>,
"ACCESS_KEY": <string>,
"PROFILE_ID": "edx",
"PURCHASE_ENDPOINT": "/shoppingcart/payment_fake"
}
}
"""
def __init__(self, browser, course_id):
"""Initialize the page.
Arguments:
browser (Browser): The browser instance.
course_id (unicode): The course in which the user is enrolling.
"""
super(FakePaymentPage, self).__init__(browser)
self._course_id = course_id
url = BASE_URL + "/shoppingcart/payment_fake/"
def is_browser_on_page(self):
"""Check if a step in the payment and verification flow has loaded."""
message = self.q(css='BODY').text[0]
match = re.search('Payment page', message)
return True if match else False
def submit_payment(self):
"""Interact with the payment submission button."""
self.q(css="input[value='Submit']").click()
return PaymentAndVerificationFlow(self.browser, self._course_id, entry_point='payment-confirmation').wait_for_page()

View File

@@ -0,0 +1,72 @@
"""Track selection page"""
from urllib import urlencode
from bok_choy.page_object import PageObject, unguarded
from bok_choy.promise import Promise, EmptyPromise
from . import BASE_URL
from .dashboard import DashboardPage
from .pay_and_verify import PaymentAndVerificationFlow
class TrackSelectionPage(PageObject):
"""Interact with the track selection page.
This page can be accessed at `/course_modes/choose/{course_id}/`.
"""
def __init__(self, browser, course_id, separate_verified=False):
"""Initialize the page.
Arguments:
browser (Browser): The browser instance.
course_id (unicode): The course in which the user is enrolling.
Keyword Arguments:
separate_verified (Boolean): Whether to use the split payment and
verification flow when enrolling as verified.
"""
super(TrackSelectionPage, self).__init__(browser)
self._course_id = course_id
self._separate_verified = separate_verified
if self._separate_verified:
self._querystring = "?separate-verified=1"
else:
self._querystring = "?disable-separate-verified=1"
@property
def url(self):
"""Return the URL corresponding to the track selection page."""
url = "{base}/course_modes/choose/{course_id}{querystring}".format(
base=BASE_URL,
course_id=self._course_id,
querystring=self._querystring
)
return url
def is_browser_on_page(self):
"""Check if the track selection page has loaded."""
return self.q(css=".wrapper-register-choose").is_present()
def enroll(self, mode="honor"):
"""Interact with one of the enrollment buttons on the page.
Keyword Arguments:
mode (str): Can be "honor" or "verified"
Raises:
ValueError
"""
if mode == "honor":
self.q(css="input[name='honor_mode']").click()
return DashboardPage(self.browser, separate_verified=self._separate_verified).wait_for_page()
elif mode == "verified":
# Check the first contribution option, then click the enroll button
self.q(css=".contribution-option > input").first.click()
self.q(css="input[name='verified_mode']").click()
return PaymentAndVerificationFlow(self.browser, self._course_id).wait_for_page()
else:
raise ValueError("Mode must be either 'honor' or 'verified'.")

View File

@@ -16,6 +16,7 @@ from ..helpers import (
select_option_by_value,
)
from ...pages.lms.auto_auth import AutoAuthPage
from ...pages.lms.create_mode import ModeCreationPage
from ...pages.common.logout import LogoutPage
from ...pages.lms.find_courses import FindCoursesPage
from ...pages.lms.course_about import CourseAboutPage
@@ -28,6 +29,8 @@ from ...pages.lms.problem import ProblemPage
from ...pages.lms.video.video import VideoPage
from ...pages.lms.courseware import CoursewarePage
from ...pages.lms.login_and_register import CombinedLoginAndRegisterPage
from ...pages.lms.track_selection import TrackSelectionPage
from ...pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentPage
from ...pages.studio.settings import SettingsPage
from ...fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
@@ -237,6 +240,128 @@ class RegisterFromCombinedPageTest(UniqueCourseTest):
self.assertEqual(self.register_page.current_form, "login")
@attr('shard_1')
class PayAndVerifyTest(UniqueCourseTest):
"""Test that we can proceed through the payment and verification flow."""
def setUp(self):
"""Initialize the test.
Create the necessary page objects, create a test course and configure its modes,
create a user and log them in.
"""
super(PayAndVerifyTest, self).setUp()
self.track_selection_page = TrackSelectionPage(self.browser, self.course_id, separate_verified=True)
self.payment_and_verification_flow = PaymentAndVerificationFlow(self.browser, self.course_id)
self.immediate_verification_page = PaymentAndVerificationFlow(self.browser, self.course_id, entry_point='verify-now')
self.upgrade_page = PaymentAndVerificationFlow(self.browser, self.course_id, entry_point='upgrade')
self.fake_payment_page = FakePaymentPage(self.browser, self.course_id)
self.dashboard_page = DashboardPage(self.browser, separate_verified=True)
# Create a course
CourseFixture(
self.course_info['org'],
self.course_info['number'],
self.course_info['run'],
self.course_info['display_name']
).install()
# Add an honor mode to the course
ModeCreationPage(self.browser, self.course_id).visit()
# Add a verified mode to the course
ModeCreationPage(self.browser, self.course_id, mode_slug=u'verified', mode_display_name=u'Verified Certificate', min_price=10, suggested_prices='10,20').visit()
def test_immediate_verification_enrollment(self):
# Create a user and log them in
AutoAuthPage(self.browser).visit()
# Navigate to the track selection page with the appropriate GET parameter in the URL
self.track_selection_page.visit()
# Enter the payment and verification flow by choosing to enroll as verified
self.track_selection_page.enroll('verified')
# Proceed to the fake payment page
self.payment_and_verification_flow.proceed_to_payment()
# Submit payment
self.fake_payment_page.submit_payment()
# Proceed to verification
self.payment_and_verification_flow.immediate_verification()
# Take face photo and proceed to the ID photo step
self.payment_and_verification_flow.webcam_capture()
self.payment_and_verification_flow.next_verification_step(self.immediate_verification_page)
# Take ID photo and proceed to the review photos step
self.payment_and_verification_flow.webcam_capture()
self.payment_and_verification_flow.next_verification_step(self.immediate_verification_page)
# Submit photos and proceed to the enrollment confirmation step
self.payment_and_verification_flow.next_verification_step(self.immediate_verification_page)
# Navigate to the dashboard with the appropriate GET parameter in the URL
self.dashboard_page.visit()
# Expect that we're enrolled as verified in the course
enrollment_mode = self.dashboard_page.get_enrollment_mode(self.course_info["display_name"])
self.assertEqual(enrollment_mode, 'verified')
def test_deferred_verification_enrollment(self):
# Create a user and log them in
AutoAuthPage(self.browser).visit()
# Navigate to the track selection page with the appropriate GET parameter in the URL
self.track_selection_page.visit()
# Enter the payment and verification flow by choosing to enroll as verified
self.track_selection_page.enroll('verified')
# Proceed to the fake payment page
self.payment_and_verification_flow.proceed_to_payment()
# Submit payment
self.fake_payment_page.submit_payment()
# Navigate to the dashboard with the appropriate GET parameter in the URL
self.dashboard_page.visit()
# Expect that we're enrolled as verified in the course
enrollment_mode = self.dashboard_page.get_enrollment_mode(self.course_info["display_name"])
self.assertEqual(enrollment_mode, 'verified')
def test_enrollment_upgrade(self):
# Create a user, log them in, and enroll them in the honor mode
AutoAuthPage(self.browser, course_id=self.course_id).visit()
# Navigate to the dashboard with the appropriate GET parameter in the URL
self.dashboard_page.visit()
# Expect that we're enrolled as honor in the course
enrollment_mode = self.dashboard_page.get_enrollment_mode(self.course_info["display_name"])
self.assertEqual(enrollment_mode, 'honor')
# Click the upsell button on the dashboard
self.dashboard_page.upgrade_enrollment(self.course_info["display_name"], self.upgrade_page)
# Select the first contribution option appearing on the page
self.upgrade_page.indicate_contribution()
# Proceed to the fake payment page
self.upgrade_page.proceed_to_payment()
# Submit payment
self.fake_payment_page.submit_payment()
# Navigate to the dashboard with the appropriate GET parameter in the URL
self.dashboard_page.visit()
# Expect that we're enrolled as verified in the course
enrollment_mode = self.dashboard_page.get_enrollment_mode(self.course_info["display_name"])
self.assertEqual(enrollment_mode, 'verified')
class LanguageTest(WebAppTest):
"""
Tests that the change language functionality on the dashboard works