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:
2
AUTHORS
2
AUTHORS
@@ -76,7 +76,7 @@ Jonah Stanley <Jonah_Stanley@brown.edu>
|
||||
Slater Victoroff <slater.r.victoroff@gmail.com>
|
||||
Peter Fogg <peter.p.fogg@gmail.com>
|
||||
Bethany LaPenta <lapentab@mit.edu>
|
||||
Renzo Lucioni <renzo@renzolucioni.com>
|
||||
Renzo Lucioni <renzo@edx.org>
|
||||
Felix Sun <felixsun@mit.edu>
|
||||
Adam Palay <adam@edx.org>
|
||||
Ian Hoover <ihoover@edx.org>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import unittest
|
||||
import decimal
|
||||
import ddt
|
||||
from mock import patch
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -9,10 +10,12 @@ from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase, mixed_store_config
|
||||
)
|
||||
|
||||
from util.testing import UrlResetMixin
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from student.models import CourseEnrollment
|
||||
from course_modes.models import CourseMode, Mode
|
||||
|
||||
|
||||
# Since we don't need any XML course fixtures, use a modulestore configuration
|
||||
@@ -23,10 +26,10 @@ MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, incl
|
||||
@ddt.ddt
|
||||
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class CourseModeViewTest(ModuleStoreTestCase):
|
||||
|
||||
class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
|
||||
@patch.dict(settings.FEATURES, {'MODE_CREATION_FOR_TESTING': True})
|
||||
def setUp(self):
|
||||
super(CourseModeViewTest, self).setUp()
|
||||
super(CourseModeViewTest, self).setUp('course_modes.urls')
|
||||
self.course = CourseFactory.create()
|
||||
self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx")
|
||||
self.client.login(username=self.user.username, password="edx")
|
||||
@@ -235,3 +238,65 @@ class CourseModeViewTest(ModuleStoreTestCase):
|
||||
response = self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE['unsupported'])
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
def test_default_mode_creation(self):
|
||||
# Hit the mode creation endpoint with no querystring params, to create an honor mode
|
||||
url = reverse('create_mode', args=[unicode(self.course.id)])
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
expected_mode = [Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None)]
|
||||
course_mode = CourseMode.modes_for_course(self.course.id)
|
||||
|
||||
self.assertEquals(course_mode, expected_mode)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@ddt.data(
|
||||
(u'verified', u'Verified Certificate', 10, '10,20,30', 'usd'),
|
||||
(u'professional', u'Professional Education', 100, '100,200', 'usd'),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_verified_mode_creation(self, mode_slug, mode_display_name, min_price, suggested_prices, currency):
|
||||
parameters = {}
|
||||
parameters['mode_slug'] = mode_slug
|
||||
parameters['mode_display_name'] = mode_display_name
|
||||
parameters['min_price'] = min_price
|
||||
parameters['suggested_prices'] = suggested_prices
|
||||
parameters['currency'] = currency
|
||||
|
||||
url = reverse('create_mode', args=[unicode(self.course.id)])
|
||||
response = self.client.get(url, parameters)
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
expected_mode = [Mode(mode_slug, mode_display_name, min_price, suggested_prices, currency, None, None)]
|
||||
course_mode = CourseMode.modes_for_course(self.course.id)
|
||||
|
||||
self.assertEquals(course_mode, expected_mode)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
def test_multiple_mode_creation(self):
|
||||
# Create an honor mode
|
||||
base_url = reverse('create_mode', args=[unicode(self.course.id)])
|
||||
self.client.get(base_url)
|
||||
|
||||
# Excluding the currency parameter implicitly tests the mode creation endpoint's ability to
|
||||
# use default values when parameters are partially missing.
|
||||
parameters = {}
|
||||
parameters['mode_slug'] = u'verified'
|
||||
parameters['mode_display_name'] = u'Verified Certificate'
|
||||
parameters['min_price'] = 10
|
||||
parameters['suggested_prices'] = '10,20'
|
||||
|
||||
# Create a verified mode
|
||||
url = reverse('create_mode', args=[unicode(self.course.id)])
|
||||
response = self.client.get(url, parameters)
|
||||
|
||||
honor_mode = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None)
|
||||
verified_mode = Mode(u'verified', u'Verified Certificate', 10, '10,20', 'usd', None, None)
|
||||
expected_modes = [honor_mode, verified_mode]
|
||||
course_modes = CourseMode.modes_for_course(self.course.id)
|
||||
|
||||
self.assertEquals(course_modes, expected_modes)
|
||||
|
||||
@@ -8,5 +8,11 @@ from course_modes import views
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
# pylint seems to dislike as_view() calls because it's a `classonlymethod` instead of `classmethod`, so we disable the warning
|
||||
url(r'^choose/{}/$'.format(settings.COURSE_ID_PATTERN), views.ChooseModeView.as_view(), name="course_modes_choose"), # pylint: disable=no-value-for-parameter
|
||||
url(r'^choose/{}/$'.format(settings.COURSE_ID_PATTERN), views.ChooseModeView.as_view(), name='course_modes_choose'), # pylint: disable=no-value-for-parameter
|
||||
)
|
||||
|
||||
# Enable verified mode creation
|
||||
if settings.FEATURES.get('MODE_CREATION_FOR_TESTING'):
|
||||
urlpatterns += (
|
||||
url(r'^create_mode/{}/$'.format(settings.COURSE_ID_PATTERN), 'course_modes.views.create_mode', name='create_mode'),
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ Views for the course_mode module
|
||||
import decimal
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import redirect
|
||||
from django.views.generic.base import View
|
||||
from django.utils.translation import ugettext as _
|
||||
@@ -226,3 +226,48 @@ class ChooseModeView(View):
|
||||
return 'honor'
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def create_mode(request, course_id):
|
||||
"""Add a mode to the course corresponding to the given course ID.
|
||||
|
||||
Only available when settings.FEATURES['MODE_CREATION_FOR_TESTING'] is True.
|
||||
|
||||
Attempts to use the following querystring parameters from the request:
|
||||
`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.
|
||||
|
||||
By default, this endpoint 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.
|
||||
|
||||
Args:
|
||||
request (`Request`): The Django Request object.
|
||||
course_id (unicode): The slash-separated course key.
|
||||
|
||||
Returns:
|
||||
Response
|
||||
"""
|
||||
PARAMETERS = {
|
||||
'mode_slug': u'honor',
|
||||
'mode_display_name': u'Honor Code Certificate',
|
||||
'min_price': 0,
|
||||
'suggested_prices': u'',
|
||||
'currency': u'usd',
|
||||
}
|
||||
|
||||
# Try pulling querystring parameters out of the request
|
||||
for parameter, default in PARAMETERS.iteritems():
|
||||
PARAMETERS[parameter] = request.GET.get(parameter, default)
|
||||
|
||||
# Attempt to create the new mode for the given course
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
CourseMode.objects.get_or_create(course_id=course_key, **PARAMETERS)
|
||||
|
||||
# Return a success message and a 200 response
|
||||
return HttpResponse("Mode '{mode_slug}' created for course with ID '{course_id}'.".format(
|
||||
mode_slug=PARAMETERS['mode_slug'],
|
||||
course_id=course_id
|
||||
))
|
||||
|
||||
71
common/test/acceptance/pages/lms/create_mode.py
Normal file
71
common/test/acceptance/pages/lms/create_mode.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
168
common/test/acceptance/pages/lms/pay_and_verify.py
Normal file
168
common/test/acceptance/pages/lms/pay_and_verify.py
Normal 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()
|
||||
72
common/test/acceptance/pages/lms/track_selection.py
Normal file
72
common/test/acceptance/pages/lms/track_selection.py
Normal 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'.")
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
"ANALYTICS_API_KEY": "",
|
||||
"AWS_ACCESS_KEY_ID": "",
|
||||
"AWS_SECRET_ACCESS_KEY": "",
|
||||
"CC_PROCESSOR_NAME": "CyberSource2",
|
||||
"CC_PROCESSOR": {
|
||||
"CyberSource2": {
|
||||
"SECRET_KEY": "abcd123",
|
||||
"ACCESS_KEY": "abcd123",
|
||||
"PROFILE_ID": "edx",
|
||||
"PURCHASE_ENDPOINT": "/shoppingcart/payment_fake"
|
||||
}
|
||||
},
|
||||
"CELERY_BROKER_PASSWORD": "celery",
|
||||
"CELERY_BROKER_USER": "celery",
|
||||
"CONTENTSTORE": {
|
||||
|
||||
@@ -65,15 +65,21 @@
|
||||
"FEATURES": {
|
||||
"AUTH_USE_OPENID_PROVIDER": true,
|
||||
"CERTIFICATES_ENABLED": true,
|
||||
"MULTIPLE_ENROLLMENT_ROLES": true,
|
||||
"ENABLE_PAYMENT_FAKE": true,
|
||||
"ENABLE_VERIFIED_CERTIFICATES": true,
|
||||
"ENABLE_DISCUSSION_SERVICE": true,
|
||||
"ENABLE_INSTRUCTOR_ANALYTICS": true,
|
||||
"ENABLE_S3_GRADE_DOWNLOADS": true,
|
||||
"ENABLE_THIRD_PARTY_AUTH": true,
|
||||
"ENABLE_COMBINED_LOGIN_REGISTRATION": true,
|
||||
"SEPARATE_VERIFICATION_FROM_PAYMENT": true,
|
||||
"PREVIEW_LMS_BASE": "localhost:8003",
|
||||
"SUBDOMAIN_BRANDING": false,
|
||||
"SUBDOMAIN_COURSE_LISTINGS": false,
|
||||
"ALLOW_AUTOMATED_SIGNUPS": true
|
||||
"ALLOW_AUTOMATED_SIGNUPS": true,
|
||||
"MODE_CREATION_FOR_TESTING": true,
|
||||
"AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING": true
|
||||
},
|
||||
"FEEDBACK_SUBMISSION_EMAIL": "",
|
||||
"GITHUB_REPO_ROOT": "** OVERRIDDEN **",
|
||||
|
||||
@@ -312,7 +312,7 @@ FEATURES = {
|
||||
# Show the mobile app links in the footer
|
||||
'ENABLE_FOOTER_MOBILE_APP_LINKS': False,
|
||||
|
||||
# let students save and manage their annotations
|
||||
# Let students save and manage their annotations
|
||||
'ENABLE_EDXNOTES': False,
|
||||
|
||||
# Milestones application flag
|
||||
@@ -320,6 +320,9 @@ FEATURES = {
|
||||
|
||||
# Prerequisite courses feature flag
|
||||
'ENABLE_PREREQUISITE_COURSES': False,
|
||||
|
||||
# For easily adding modes to courses during acceptance testing
|
||||
'MODE_CREATION_FOR_TESTING': False,
|
||||
}
|
||||
|
||||
# Ignore static asset files on import which match this pattern
|
||||
|
||||
@@ -70,6 +70,12 @@ var edx = edx || {};
|
||||
platformName: el.data('platform-name'),
|
||||
requirements: el.data('requirements')
|
||||
},
|
||||
'face-photo-step': {
|
||||
platformName: el.data('platform-name')
|
||||
},
|
||||
'id-photo-step': {
|
||||
platformName: el.data('platform-name')
|
||||
},
|
||||
'review-photos-step': {
|
||||
fullName: el.data('full-name'),
|
||||
platformName: el.data('platform-name')
|
||||
@@ -79,12 +85,6 @@ var edx = edx || {};
|
||||
courseStartDate: el.data('course-start-date'),
|
||||
coursewareUrl: el.data('courseware-url'),
|
||||
platformName: el.data('platform-name')
|
||||
},
|
||||
'face-photo-step': {
|
||||
platformName: el.data('platform-name')
|
||||
},
|
||||
'id-photo-step': {
|
||||
platformName: el.data('platform-name')
|
||||
}
|
||||
}
|
||||
}).render();
|
||||
|
||||
@@ -25,7 +25,14 @@
|
||||
|
||||
// Start the capture
|
||||
this.getUserMediaFunc()(
|
||||
{ video: true },
|
||||
{
|
||||
video: true,
|
||||
|
||||
// Specify the `fake` constraint if we detect we are running in a test
|
||||
// environment. In Chrome, this will do nothing, but in Firefox, it will
|
||||
// instruct the browser to use a fake video device.
|
||||
fake: window.location.hostname === 'localhost'
|
||||
},
|
||||
_.bind( this.getUserMediaCallback, this ),
|
||||
_.bind( this.handleVideoFailure, this )
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a
|
||||
-e git+https://github.com/edx/codejail.git@2b095e820ff752a108653bb39d518b122f7154db#egg=codejail
|
||||
-e git+https://github.com/edx/js-test-tool.git@v0.1.6#egg=js_test_tool
|
||||
-e git+https://github.com/edx/event-tracking.git@0.1.0#egg=event-tracking
|
||||
-e git+https://github.com/edx/bok-choy.git@4a259e3548a19e41cc39433caf68ea58d10a27ba#egg=bok_choy
|
||||
-e git+https://github.com/edx/bok-choy.git@9fd05383dc434e0275c9874fd50ad14e519448bc#egg=bok_choy
|
||||
-e git+https://github.com/edx-solutions/django-splash.git@7579d052afcf474ece1239153cffe1c89935bc4f#egg=django-splash
|
||||
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
|
||||
-e git+https://github.com/edx/edx-ora2.git@release-2015-01-05T12.24#egg=edx-ora2
|
||||
|
||||
Reference in New Issue
Block a user