Block users from enrolling in a course if the user is blocked by country access rules. 1) Enrollment via the login/registration page. 2) Enrollment from the marketing iframe (via student.views.change_enrollment) 3) Enrollment using 100% redeem codes. 4) Enrollment via upgrade. This does NOT cover enrollment through third party authentication, which is sufficiently complex to deserve its own commit.
1567 lines
62 KiB
Python
1567 lines
62 KiB
Python
# encoding: utf-8
|
|
"""
|
|
Tests of verify_student views.
|
|
"""
|
|
import json
|
|
import mock
|
|
import urllib
|
|
import decimal
|
|
from mock import patch, Mock
|
|
import pytz
|
|
from datetime import timedelta, datetime
|
|
|
|
import ddt
|
|
from django.test.client import Client
|
|
from django.test import TestCase
|
|
from django.test.utils import override_settings
|
|
from django.conf import settings
|
|
from django.core.urlresolvers import reverse
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from django.core import mail
|
|
from bs4 import BeautifulSoup
|
|
|
|
from openedx.core.djangoapps.user_api.api import profile as profile_api
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_store_config
|
|
from xmodule.modulestore.tests.factories import CourseFactory
|
|
from xmodule.modulestore.django import modulestore
|
|
from xmodule.modulestore import ModuleStoreEnum
|
|
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
|
from opaque_keys.edx.locator import CourseLocator
|
|
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
|
from student.models import CourseEnrollment
|
|
from course_modes.tests.factories import CourseModeFactory
|
|
from course_modes.models import CourseMode
|
|
from shoppingcart.models import Order, CertificateItem
|
|
from embargo.test_utils import restrict_course
|
|
from util.testing import UrlResetMixin
|
|
from verify_student.views import render_to_response, PayAndVerifyView
|
|
from verify_student.models import SoftwareSecurePhotoVerification
|
|
from reverification.tests.factories import MidcourseReverificationWindowFactory
|
|
|
|
|
|
def mock_render_to_response(*args, **kwargs):
|
|
return render_to_response(*args, **kwargs)
|
|
|
|
render_mock = Mock(side_effect=mock_render_to_response)
|
|
|
|
|
|
class StartView(TestCase):
|
|
def start_url(self, course_id=""):
|
|
return "/verify_student/{0}".format(urllib.quote(course_id))
|
|
|
|
def test_start_new_verification(self):
|
|
"""
|
|
Test the case where the user has no pending `PhotoVerificationAttempts`,
|
|
but is just starting their first.
|
|
"""
|
|
user = UserFactory.create(username="rusty", password="test")
|
|
self.client.login(username="rusty", password="test")
|
|
|
|
def must_be_logged_in(self):
|
|
self.assertHttpForbidden(self.client.get(self.start_url()))
|
|
|
|
|
|
@ddt.ddt
|
|
class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
|
|
"""
|
|
Tests for the payment and verification flow views.
|
|
"""
|
|
MIN_PRICE = 12
|
|
USERNAME = "test_user"
|
|
PASSWORD = "test_password"
|
|
|
|
NOW = datetime.now(pytz.UTC)
|
|
YESTERDAY = NOW - timedelta(days=1)
|
|
TOMORROW = NOW + timedelta(days=1)
|
|
|
|
@mock.patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
|
|
def setUp(self):
|
|
super(TestPayAndVerifyView, self).setUp('embargo')
|
|
self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD)
|
|
result = self.client.login(username=self.USERNAME, password=self.PASSWORD)
|
|
self.assertTrue(result, msg="Could not log in")
|
|
|
|
@ddt.data("verified", "professional")
|
|
def test_start_flow_not_verified(self, course_mode):
|
|
course = self._create_course(course_mode)
|
|
self._enroll(course.id, "honor")
|
|
response = self._get_page('verify_student_start_flow', course.id)
|
|
self._assert_displayed_mode(response, course_mode)
|
|
self._assert_steps_displayed(
|
|
response,
|
|
PayAndVerifyView.PAYMENT_STEPS + PayAndVerifyView.VERIFICATION_STEPS,
|
|
PayAndVerifyView.MAKE_PAYMENT_STEP
|
|
)
|
|
self._assert_messaging(response, PayAndVerifyView.FIRST_TIME_VERIFY_MSG)
|
|
self._assert_requirements_displayed(response, [
|
|
PayAndVerifyView.PHOTO_ID_REQ,
|
|
PayAndVerifyView.WEBCAM_REQ,
|
|
])
|
|
self._assert_upgrade_session_flag(False)
|
|
|
|
@ddt.data("expired", "denied")
|
|
def test_start_flow_expired_or_denied_verification(self, verification_status):
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "verified")
|
|
self._set_verification_status(verification_status)
|
|
response = self._get_page('verify_student_start_flow', course.id)
|
|
|
|
# Expect the same content as when the user has not verified
|
|
self._assert_steps_displayed(
|
|
response,
|
|
[PayAndVerifyView.INTRO_STEP] + PayAndVerifyView.VERIFICATION_STEPS,
|
|
PayAndVerifyView.INTRO_STEP
|
|
)
|
|
self._assert_messaging(response, PayAndVerifyView.FIRST_TIME_VERIFY_MSG)
|
|
self._assert_requirements_displayed(response, [
|
|
PayAndVerifyView.PHOTO_ID_REQ,
|
|
PayAndVerifyView.WEBCAM_REQ,
|
|
])
|
|
|
|
@ddt.data(
|
|
("verified", "submitted"),
|
|
("verified", "approved"),
|
|
("verified", "error"),
|
|
("professional", "submitted")
|
|
)
|
|
@ddt.unpack
|
|
def test_start_flow_already_verified(self, course_mode, verification_status):
|
|
course = self._create_course(course_mode)
|
|
self._enroll(course.id, "honor")
|
|
self._set_verification_status(verification_status)
|
|
response = self._get_page('verify_student_start_flow', course.id)
|
|
self._assert_displayed_mode(response, course_mode)
|
|
self._assert_steps_displayed(
|
|
response,
|
|
PayAndVerifyView.PAYMENT_STEPS,
|
|
PayAndVerifyView.MAKE_PAYMENT_STEP
|
|
)
|
|
self._assert_messaging(response, PayAndVerifyView.FIRST_TIME_VERIFY_MSG)
|
|
self._assert_requirements_displayed(response, [])
|
|
|
|
@ddt.data("verified", "professional")
|
|
def test_start_flow_already_paid(self, course_mode):
|
|
course = self._create_course(course_mode)
|
|
self._enroll(course.id, course_mode)
|
|
response = self._get_page('verify_student_start_flow', course.id)
|
|
self._assert_displayed_mode(response, course_mode)
|
|
self._assert_steps_displayed(
|
|
response,
|
|
[PayAndVerifyView.INTRO_STEP] + PayAndVerifyView.VERIFICATION_STEPS,
|
|
PayAndVerifyView.INTRO_STEP
|
|
)
|
|
self._assert_messaging(response, PayAndVerifyView.FIRST_TIME_VERIFY_MSG)
|
|
self._assert_requirements_displayed(response, [
|
|
PayAndVerifyView.PHOTO_ID_REQ,
|
|
PayAndVerifyView.WEBCAM_REQ,
|
|
])
|
|
|
|
def test_start_flow_not_enrolled(self):
|
|
course = self._create_course("verified")
|
|
self._set_verification_status("submitted")
|
|
response = self._get_page('verify_student_start_flow', course.id)
|
|
|
|
# This shouldn't happen if the student has been auto-enrolled,
|
|
# but if they somehow end up on this page without enrolling,
|
|
# treat them as if they need to pay
|
|
response = self._get_page('verify_student_start_flow', course.id)
|
|
self._assert_steps_displayed(
|
|
response,
|
|
PayAndVerifyView.PAYMENT_STEPS,
|
|
PayAndVerifyView.MAKE_PAYMENT_STEP
|
|
)
|
|
self._assert_requirements_displayed(response, [])
|
|
|
|
def test_start_flow_unenrolled(self):
|
|
course = self._create_course("verified")
|
|
self._set_verification_status("submitted")
|
|
self._enroll(course.id, "verified")
|
|
self._unenroll(course.id)
|
|
|
|
# If unenrolled, treat them like they haven't paid at all
|
|
# (we assume that they've gotten a refund or didn't pay initially)
|
|
response = self._get_page('verify_student_start_flow', course.id)
|
|
self._assert_steps_displayed(
|
|
response,
|
|
PayAndVerifyView.PAYMENT_STEPS,
|
|
PayAndVerifyView.MAKE_PAYMENT_STEP
|
|
)
|
|
self._assert_requirements_displayed(response, [])
|
|
|
|
@ddt.data(
|
|
("verified", "submitted"),
|
|
("verified", "approved"),
|
|
("professional", "submitted")
|
|
)
|
|
@ddt.unpack
|
|
def test_start_flow_already_verified_and_paid(self, course_mode, verification_status):
|
|
course = self._create_course(course_mode)
|
|
self._enroll(course.id, course_mode)
|
|
self._set_verification_status(verification_status)
|
|
response = self._get_page(
|
|
'verify_student_start_flow',
|
|
course.id,
|
|
expected_status_code=302
|
|
)
|
|
self._assert_redirects_to_dashboard(response)
|
|
|
|
def test_verify_now(self):
|
|
# We've already paid, and now we're trying to verify
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "verified")
|
|
response = self._get_page('verify_student_verify_now', course.id)
|
|
|
|
self._assert_messaging(response, PayAndVerifyView.VERIFY_NOW_MSG)
|
|
|
|
# Expect that *all* steps are displayed,
|
|
# but we start after the payment step (because it's already completed).
|
|
self._assert_steps_displayed(
|
|
response,
|
|
PayAndVerifyView.PAYMENT_STEPS + PayAndVerifyView.VERIFICATION_STEPS,
|
|
PayAndVerifyView.FACE_PHOTO_STEP
|
|
)
|
|
|
|
# These will be hidden from the user anyway since they're starting
|
|
# after the payment step.
|
|
self._assert_requirements_displayed(response, [
|
|
PayAndVerifyView.PHOTO_ID_REQ,
|
|
PayAndVerifyView.WEBCAM_REQ,
|
|
])
|
|
|
|
def test_verify_now_already_verified(self):
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "verified")
|
|
self._set_verification_status("submitted")
|
|
|
|
# Already verified, so if we somehow end up here,
|
|
# redirect immediately to the dashboard
|
|
response = self._get_page(
|
|
'verify_student_verify_now',
|
|
course.id,
|
|
expected_status_code=302
|
|
)
|
|
self._assert_redirects_to_dashboard(response)
|
|
|
|
def test_verify_now_user_details(self):
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "verified")
|
|
response = self._get_page('verify_student_verify_now', course.id)
|
|
self._assert_user_details(response, self.user.profile.name)
|
|
|
|
@ddt.data(
|
|
"verify_student_verify_now",
|
|
"verify_student_verify_later",
|
|
"verify_student_payment_confirmation"
|
|
)
|
|
def test_verify_now_or_later_not_enrolled(self, page_name):
|
|
course = self._create_course("verified")
|
|
response = self._get_page(page_name, course.id, expected_status_code=302)
|
|
self._assert_redirects_to_start_flow(response, course.id)
|
|
|
|
@ddt.data(
|
|
"verify_student_verify_now",
|
|
"verify_student_verify_later",
|
|
"verify_student_payment_confirmation"
|
|
)
|
|
def test_verify_now_or_later_unenrolled(self, page_name):
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "verified")
|
|
self._unenroll(course.id)
|
|
response = self._get_page(page_name, course.id, expected_status_code=302)
|
|
self._assert_redirects_to_start_flow(response, course.id)
|
|
|
|
@ddt.data(
|
|
"verify_student_verify_now",
|
|
"verify_student_verify_later",
|
|
"verify_student_payment_confirmation"
|
|
)
|
|
def test_verify_now_or_later_not_paid(self, page_name):
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "honor")
|
|
response = self._get_page(page_name, course.id, expected_status_code=302)
|
|
self._assert_redirects_to_upgrade(response, course.id)
|
|
|
|
def test_verify_later(self):
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "verified")
|
|
response = self._get_page("verify_student_verify_later", course.id)
|
|
|
|
self._assert_messaging(response, PayAndVerifyView.VERIFY_LATER_MSG)
|
|
|
|
# Expect that the payment steps are NOT displayed
|
|
self._assert_steps_displayed(
|
|
response,
|
|
[PayAndVerifyView.INTRO_STEP] + PayAndVerifyView.VERIFICATION_STEPS,
|
|
PayAndVerifyView.INTRO_STEP
|
|
)
|
|
self._assert_requirements_displayed(response, [
|
|
PayAndVerifyView.PHOTO_ID_REQ,
|
|
PayAndVerifyView.WEBCAM_REQ,
|
|
])
|
|
|
|
def test_verify_later_already_verified(self):
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "verified")
|
|
self._set_verification_status("submitted")
|
|
|
|
# Already verified, so if we somehow end up here,
|
|
# redirect immediately to the dashboard
|
|
response = self._get_page(
|
|
'verify_student_verify_later',
|
|
course.id,
|
|
expected_status_code=302
|
|
)
|
|
self._assert_redirects_to_dashboard(response)
|
|
|
|
def test_payment_confirmation(self):
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "verified")
|
|
response = self._get_page('verify_student_payment_confirmation', course.id)
|
|
|
|
self._assert_messaging(response, PayAndVerifyView.PAYMENT_CONFIRMATION_MSG)
|
|
|
|
# Expect that *all* steps are displayed,
|
|
# but we start at the payment confirmation step
|
|
self._assert_steps_displayed(
|
|
response,
|
|
PayAndVerifyView.PAYMENT_STEPS + PayAndVerifyView.VERIFICATION_STEPS,
|
|
PayAndVerifyView.PAYMENT_CONFIRMATION_STEP,
|
|
)
|
|
|
|
# These will be hidden from the user anyway since they're starting
|
|
# after the payment step. We're already including the payment
|
|
# steps, so it's easier to include these as well.
|
|
self._assert_requirements_displayed(response, [
|
|
PayAndVerifyView.PHOTO_ID_REQ,
|
|
PayAndVerifyView.WEBCAM_REQ,
|
|
])
|
|
|
|
def test_payment_cannot_skip(self):
|
|
"""
|
|
Simple test to verify that certain steps cannot be skipped. This test sets up
|
|
a scenario where the user should be on the MAKE_PAYMENT_STEP, but is trying to
|
|
skip it. Despite setting the parameter, the current step should still be
|
|
MAKE_PAYMENT_STEP.
|
|
"""
|
|
course = self._create_course("verified")
|
|
response = self._get_page(
|
|
'verify_student_start_flow',
|
|
course.id,
|
|
skip_first_step=True
|
|
)
|
|
|
|
self._assert_messaging(response, PayAndVerifyView.FIRST_TIME_VERIFY_MSG)
|
|
|
|
# Expect that *all* steps are displayed,
|
|
# but we start on the first verify step
|
|
self._assert_steps_displayed(
|
|
response,
|
|
PayAndVerifyView.PAYMENT_STEPS + PayAndVerifyView.VERIFICATION_STEPS,
|
|
PayAndVerifyView.MAKE_PAYMENT_STEP,
|
|
)
|
|
|
|
def test_payment_confirmation_already_verified(self):
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "verified")
|
|
self._set_verification_status("submitted")
|
|
|
|
response = self._get_page('verify_student_payment_confirmation', course.id)
|
|
|
|
# Other pages would redirect to the dashboard at this point,
|
|
# because the user has paid and verified. However, we want
|
|
# the user to see the confirmation page even if there
|
|
# isn't anything for them to do here except return
|
|
# to the dashboard.
|
|
self._assert_steps_displayed(
|
|
response,
|
|
PayAndVerifyView.PAYMENT_STEPS,
|
|
PayAndVerifyView.PAYMENT_CONFIRMATION_STEP,
|
|
)
|
|
|
|
def test_payment_confirmation_already_verified_skip_first_step(self):
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "verified")
|
|
self._set_verification_status("submitted")
|
|
|
|
response = self._get_page(
|
|
'verify_student_payment_confirmation',
|
|
course.id,
|
|
skip_first_step=True
|
|
)
|
|
|
|
# There are no other steps, so stay on the
|
|
# payment confirmation step
|
|
self._assert_steps_displayed(
|
|
response,
|
|
PayAndVerifyView.PAYMENT_STEPS,
|
|
PayAndVerifyView.PAYMENT_CONFIRMATION_STEP,
|
|
)
|
|
|
|
@ddt.data(
|
|
(YESTERDAY, True),
|
|
(TOMORROW, False)
|
|
)
|
|
@ddt.unpack
|
|
def test_payment_confirmation_course_details(self, course_start, show_courseware_url):
|
|
course = self._create_course("verified", course_start=course_start)
|
|
self._enroll(course.id, "verified")
|
|
response = self._get_page('verify_student_payment_confirmation', course.id)
|
|
|
|
courseware_url = (
|
|
reverse("course_root", kwargs={'course_id': unicode(course.id)})
|
|
if show_courseware_url else ""
|
|
)
|
|
self._assert_course_details(
|
|
response,
|
|
unicode(course.id),
|
|
course.display_name,
|
|
course.start_datetime_text(),
|
|
courseware_url
|
|
)
|
|
|
|
@ddt.data("verified", "professional")
|
|
def test_upgrade(self, course_mode):
|
|
course = self._create_course(course_mode)
|
|
self._enroll(course.id, "honor")
|
|
|
|
response = self._get_page('verify_student_upgrade_and_verify', course.id)
|
|
self._assert_displayed_mode(response, course_mode)
|
|
self._assert_steps_displayed(
|
|
response,
|
|
PayAndVerifyView.PAYMENT_STEPS + PayAndVerifyView.VERIFICATION_STEPS,
|
|
PayAndVerifyView.MAKE_PAYMENT_STEP
|
|
)
|
|
self._assert_messaging(response, PayAndVerifyView.UPGRADE_MSG)
|
|
self._assert_requirements_displayed(response, [
|
|
PayAndVerifyView.PHOTO_ID_REQ,
|
|
PayAndVerifyView.WEBCAM_REQ,
|
|
])
|
|
self._assert_upgrade_session_flag(True)
|
|
|
|
def test_upgrade_already_verified(self):
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "honor")
|
|
self._set_verification_status("submitted")
|
|
|
|
response = self._get_page('verify_student_upgrade_and_verify', course.id)
|
|
self._assert_steps_displayed(
|
|
response,
|
|
PayAndVerifyView.PAYMENT_STEPS,
|
|
PayAndVerifyView.MAKE_PAYMENT_STEP
|
|
)
|
|
self._assert_messaging(response, PayAndVerifyView.UPGRADE_MSG)
|
|
self._assert_requirements_displayed(response, [])
|
|
|
|
def test_upgrade_already_paid(self):
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "verified")
|
|
|
|
# If we've already paid, then the upgrade messaging
|
|
# won't make much sense. Redirect them to the
|
|
# "verify later" page instead.
|
|
response = self._get_page(
|
|
'verify_student_upgrade_and_verify',
|
|
course.id,
|
|
expected_status_code=302
|
|
)
|
|
self._assert_redirects_to_verify_later(response, course.id)
|
|
|
|
def test_upgrade_already_verified_and_paid(self):
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "verified")
|
|
self._set_verification_status("submitted")
|
|
|
|
# Already verified and paid, so redirect to the dashboard
|
|
response = self._get_page(
|
|
'verify_student_upgrade_and_verify',
|
|
course.id,
|
|
expected_status_code=302
|
|
)
|
|
self._assert_redirects_to_dashboard(response)
|
|
|
|
def test_upgrade_not_enrolled(self):
|
|
course = self._create_course("verified")
|
|
response = self._get_page(
|
|
'verify_student_upgrade_and_verify',
|
|
course.id,
|
|
expected_status_code=302
|
|
)
|
|
self._assert_redirects_to_start_flow(response, course.id)
|
|
|
|
def test_upgrade_unenrolled(self):
|
|
course = self._create_course("verified")
|
|
self._enroll(course.id, "verified")
|
|
self._unenroll(course.id)
|
|
response = self._get_page(
|
|
'verify_student_upgrade_and_verify',
|
|
course.id,
|
|
expected_status_code=302
|
|
)
|
|
self._assert_redirects_to_start_flow(response, course.id)
|
|
|
|
@ddt.data([], ["honor"], ["honor", "audit"])
|
|
def test_no_verified_mode_for_course(self, modes_available):
|
|
course = self._create_course(*modes_available)
|
|
|
|
pages = [
|
|
'verify_student_start_flow',
|
|
'verify_student_verify_now',
|
|
'verify_student_verify_later',
|
|
'verify_student_upgrade_and_verify',
|
|
]
|
|
|
|
for page_name in pages:
|
|
self._get_page(
|
|
page_name,
|
|
course.id,
|
|
expected_status_code=404
|
|
)
|
|
|
|
@ddt.data(
|
|
"verify_student_start_flow",
|
|
"verify_student_verify_now",
|
|
"verify_student_verify_later",
|
|
"verify_student_upgrade_and_verify",
|
|
)
|
|
def test_require_login(self, url_name):
|
|
self.client.logout()
|
|
course = self._create_course("verified")
|
|
response = self._get_page(url_name, course.id, expected_status_code=302)
|
|
|
|
original_url = reverse(url_name, kwargs={'course_id': unicode(course.id)})
|
|
login_url = u"{login_url}?next={original_url}".format(
|
|
login_url=reverse('accounts_login'),
|
|
original_url=original_url
|
|
)
|
|
self.assertRedirects(response, login_url)
|
|
|
|
@ddt.data(
|
|
"verify_student_start_flow",
|
|
"verify_student_verify_now",
|
|
"verify_student_verify_later",
|
|
"verify_student_upgrade_and_verify",
|
|
)
|
|
def test_no_such_course(self, url_name):
|
|
non_existent_course = CourseLocator(course="test", org="test", run="test")
|
|
self._get_page(
|
|
url_name,
|
|
non_existent_course,
|
|
expected_status_code=404
|
|
)
|
|
|
|
def test_account_not_active(self):
|
|
self.user.is_active = False
|
|
self.user.save()
|
|
course = self._create_course("verified")
|
|
response = self._get_page('verify_student_start_flow', course.id)
|
|
self._assert_steps_displayed(
|
|
response,
|
|
PayAndVerifyView.PAYMENT_STEPS + PayAndVerifyView.VERIFICATION_STEPS,
|
|
PayAndVerifyView.MAKE_PAYMENT_STEP
|
|
)
|
|
self._assert_requirements_displayed(response, [
|
|
PayAndVerifyView.ACCOUNT_ACTIVATION_REQ,
|
|
PayAndVerifyView.PHOTO_ID_REQ,
|
|
PayAndVerifyView.WEBCAM_REQ,
|
|
])
|
|
|
|
def test_no_contribution(self):
|
|
# Do NOT specify a contribution for the course in a session var.
|
|
course = self._create_course("verified")
|
|
response = self._get_page("verify_student_start_flow", course.id)
|
|
self._assert_contribution_amount(response, "")
|
|
|
|
def test_contribution_other_course(self):
|
|
# Specify a contribution amount for another course in the session
|
|
course = self._create_course("verified")
|
|
other_course_id = CourseLocator(org="other", run="test", course="test")
|
|
self._set_contribution("12.34", other_course_id)
|
|
|
|
# Expect that the contribution amount is NOT pre-filled,
|
|
response = self._get_page("verify_student_start_flow", course.id)
|
|
self._assert_contribution_amount(response, "")
|
|
|
|
def test_contribution(self):
|
|
# Specify a contribution amount for this course in the session
|
|
course = self._create_course("verified")
|
|
self._set_contribution("12.34", course.id)
|
|
|
|
# Expect that the contribution amount is pre-filled,
|
|
response = self._get_page("verify_student_start_flow", course.id)
|
|
self._assert_contribution_amount(response, "12.34")
|
|
|
|
def test_verification_deadline(self):
|
|
# Set a deadline on the course mode
|
|
course = self._create_course("verified")
|
|
mode = CourseMode.objects.get(
|
|
course_id=course.id,
|
|
mode_slug="verified"
|
|
)
|
|
expiration = datetime(2999, 1, 2, tzinfo=pytz.UTC)
|
|
mode.expiration_datetime = expiration
|
|
mode.save()
|
|
|
|
# Expect that the expiration date is set
|
|
response = self._get_page("verify_student_start_flow", course.id)
|
|
data = self._get_page_data(response)
|
|
self.assertEqual(data['verification_deadline'], "Jan 02, 2999 at 00:00 UTC")
|
|
|
|
def test_course_mode_expired(self):
|
|
course = self._create_course("verified")
|
|
mode = CourseMode.objects.get(
|
|
course_id=course.id,
|
|
mode_slug="verified"
|
|
)
|
|
expiration = datetime(1999, 1, 2, tzinfo=pytz.UTC)
|
|
mode.expiration_datetime = expiration
|
|
mode.save()
|
|
|
|
# Need to be enrolled
|
|
self._enroll(course.id, "verified")
|
|
|
|
# The course mode has expired, so expect an explanation
|
|
# to the student that the deadline has passed
|
|
response = self._get_page("verify_student_verify_later", course.id)
|
|
self.assertContains(response, "verification deadline")
|
|
self.assertContains(response, "Jan 02, 1999 at 00:00 UTC")
|
|
|
|
@mock.patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
|
|
def test_embargo_restrict(self):
|
|
course = self._create_course("verified")
|
|
with restrict_course(course.id) as redirect_url:
|
|
# Simulate that we're embargoed from accessing this
|
|
# course based on our IP address.
|
|
response = self._get_page('verify_student_start_flow', course.id, expected_status_code=302)
|
|
self.assertRedirects(response, redirect_url)
|
|
|
|
@mock.patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
|
|
def test_embargo_allow(self):
|
|
course = self._create_course("verified")
|
|
self._get_page('verify_student_start_flow', course.id)
|
|
|
|
def _create_course(self, *course_modes, **kwargs):
|
|
"""Create a new course with the specified course modes. """
|
|
course = CourseFactory.create()
|
|
|
|
if kwargs.get('course_start'):
|
|
course.start = kwargs.get('course_start')
|
|
modulestore().update_item(course, ModuleStoreEnum.UserID.test)
|
|
|
|
for course_mode in course_modes:
|
|
min_price = (self.MIN_PRICE if course_mode != "honor" else 0)
|
|
CourseModeFactory(
|
|
course_id=course.id,
|
|
mode_slug=course_mode,
|
|
mode_display_name=course_mode,
|
|
min_price=min_price
|
|
)
|
|
|
|
return course
|
|
|
|
def _enroll(self, course_key, mode):
|
|
"""Enroll the user in a course. """
|
|
CourseEnrollmentFactory.create(
|
|
user=self.user,
|
|
course_id=course_key,
|
|
mode=mode
|
|
)
|
|
|
|
def _unenroll(self, course_key):
|
|
"""Unenroll the user from a course. """
|
|
CourseEnrollment.unenroll(self.user, course_key)
|
|
|
|
def _set_verification_status(self, status):
|
|
"""Set the user's photo verification status. """
|
|
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
|
|
|
if status in ["submitted", "approved", "expired", "denied", "error"]:
|
|
attempt.mark_ready()
|
|
attempt.submit()
|
|
|
|
if status in ["approved", "expired"]:
|
|
attempt.approve()
|
|
elif status == "denied":
|
|
attempt.deny("Denied!")
|
|
elif status == "error":
|
|
attempt.system_error("Error!")
|
|
|
|
if status == "expired":
|
|
days_good_for = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]
|
|
attempt.created_at = datetime.now(pytz.UTC) - timedelta(days=(days_good_for + 1))
|
|
attempt.save()
|
|
|
|
def _set_contribution(self, amount, course_id):
|
|
"""Set the contribution amount pre-filled in a session var. """
|
|
session = self.client.session
|
|
session["donation_for_course"] = {
|
|
unicode(course_id): amount
|
|
}
|
|
session.save()
|
|
|
|
def _get_page(self, url_name, course_key, expected_status_code=200, skip_first_step=False):
|
|
"""Retrieve one of the verification pages. """
|
|
url = reverse(url_name, kwargs={"course_id": unicode(course_key)})
|
|
|
|
if skip_first_step:
|
|
url += "?skip-first-step=1"
|
|
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, expected_status_code)
|
|
return response
|
|
|
|
def _assert_displayed_mode(self, response, expected_mode):
|
|
"""Check whether a course mode is displayed. """
|
|
response_dict = self._get_page_data(response)
|
|
self.assertEqual(response_dict['course_mode_slug'], expected_mode)
|
|
|
|
def _assert_steps_displayed(self, response, expected_steps, expected_current_step):
|
|
"""Check whether steps in the flow are displayed to the user. """
|
|
response_dict = self._get_page_data(response)
|
|
self.assertEqual(response_dict['current_step'], expected_current_step)
|
|
self.assertEqual(expected_steps, [
|
|
step['name'] for step in
|
|
response_dict['display_steps']
|
|
])
|
|
|
|
def _assert_messaging(self, response, expected_message):
|
|
"""Check the messaging on the page. """
|
|
response_dict = self._get_page_data(response)
|
|
self.assertEqual(response_dict['message_key'], expected_message)
|
|
|
|
def _assert_requirements_displayed(self, response, requirements):
|
|
"""Check that requirements are displayed on the page. """
|
|
response_dict = self._get_page_data(response)
|
|
for req, displayed in response_dict['requirements'].iteritems():
|
|
if req in requirements:
|
|
self.assertTrue(displayed, msg="Expected '{req}' requirement to be displayed".format(req=req))
|
|
else:
|
|
self.assertFalse(displayed, msg="Expected '{req}' requirement to be hidden".format(req=req))
|
|
|
|
def _assert_course_details(self, response, course_key, display_name, start_text, url):
|
|
"""Check the course information on the page. """
|
|
response_dict = self._get_page_data(response)
|
|
self.assertEqual(response_dict['course_key'], course_key)
|
|
self.assertEqual(response_dict['course_name'], display_name)
|
|
self.assertEqual(response_dict['course_start_date'], start_text)
|
|
self.assertEqual(response_dict['courseware_url'], url)
|
|
|
|
def _assert_user_details(self, response, full_name):
|
|
"""Check the user detail information on the page. """
|
|
response_dict = self._get_page_data(response)
|
|
self.assertEqual(response_dict['full_name'], full_name)
|
|
|
|
def _assert_contribution_amount(self, response, expected_amount):
|
|
"""Check the pre-filled contribution amount. """
|
|
response_dict = self._get_page_data(response)
|
|
self.assertEqual(response_dict['contribution_amount'], expected_amount)
|
|
|
|
def _get_page_data(self, response):
|
|
"""Retrieve the data attributes rendered on the page. """
|
|
soup = BeautifulSoup(response.content)
|
|
pay_and_verify_div = soup.find(id="pay-and-verify-container")
|
|
return {
|
|
'full_name': pay_and_verify_div['data-full-name'],
|
|
'course_key': pay_and_verify_div['data-course-key'],
|
|
'course_name': pay_and_verify_div['data-course-name'],
|
|
'course_start_date': pay_and_verify_div['data-course-start-date'],
|
|
'courseware_url': pay_and_verify_div['data-courseware-url'],
|
|
'course_mode_name': pay_and_verify_div['data-course-mode-name'],
|
|
'course_mode_slug': pay_and_verify_div['data-course-mode-slug'],
|
|
'display_steps': json.loads(pay_and_verify_div['data-display-steps']),
|
|
'current_step': pay_and_verify_div['data-current-step'],
|
|
'requirements': json.loads(pay_and_verify_div['data-requirements']),
|
|
'message_key': pay_and_verify_div['data-msg-key'],
|
|
'contribution_amount': pay_and_verify_div['data-contribution-amount'],
|
|
'verification_deadline': pay_and_verify_div['data-verification-deadline']
|
|
}
|
|
|
|
def _assert_upgrade_session_flag(self, is_upgrade):
|
|
"""Check that the session flag for attempting an upgrade is set. """
|
|
self.assertEqual(self.client.session.get('attempting_upgrade'), is_upgrade)
|
|
|
|
def _assert_redirects_to_dashboard(self, response):
|
|
"""Check that the page redirects to the student dashboard. """
|
|
self.assertRedirects(response, reverse('dashboard'))
|
|
|
|
def _assert_redirects_to_start_flow(self, response, course_id):
|
|
"""Check that the page redirects to the start of the payment/verification flow. """
|
|
url = reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_id)})
|
|
self.assertRedirects(response, url)
|
|
|
|
def _assert_redirects_to_verify_later(self, response, course_id):
|
|
"""Check that the page redirects to the "verify later" part of the flow. """
|
|
url = reverse('verify_student_verify_later', kwargs={'course_id': unicode(course_id)})
|
|
self.assertRedirects(response, url)
|
|
|
|
def _assert_redirects_to_upgrade(self, response, course_id):
|
|
"""Check that the page redirects to the "upgrade" part of the flow. """
|
|
url = reverse('verify_student_upgrade_and_verify', kwargs={'course_id': unicode(course_id)})
|
|
self.assertRedirects(response, url)
|
|
|
|
|
|
class TestCreateOrder(ModuleStoreTestCase):
|
|
"""
|
|
Tests for the create order view.
|
|
"""
|
|
def setUp(self):
|
|
""" Create a user and course. """
|
|
super(TestCreateOrder, self).setUp()
|
|
|
|
self.user = UserFactory.create(username="test", password="test")
|
|
self.course = CourseFactory.create()
|
|
for mode in ('audit', 'honor', 'verified'):
|
|
CourseModeFactory(mode_slug=mode, course_id=self.course.id)
|
|
self.client.login(username="test", password="test")
|
|
|
|
def test_create_order_already_verified(self):
|
|
# Verify the student so we don't need to submit photos
|
|
self._verify_student()
|
|
|
|
# Create an order
|
|
url = reverse('verify_student_create_order')
|
|
params = {
|
|
'course_id': unicode(self.course.id),
|
|
}
|
|
response = self.client.post(url, params)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# Verify that the information will be sent to the correct callback URL
|
|
# (configured by test settings)
|
|
data = json.loads(response.content)
|
|
self.assertEqual(data['override_custom_receipt_page'], "http://testserver/shoppingcart/postpay_callback/")
|
|
|
|
# Verify that the course ID and transaction type are included in "merchant-defined data"
|
|
self.assertEqual(data['merchant_defined_data1'], unicode(self.course.id))
|
|
self.assertEqual(data['merchant_defined_data2'], "verified")
|
|
|
|
def test_create_order_already_verified_prof_ed(self):
|
|
# Verify the student so we don't need to submit photos
|
|
self._verify_student()
|
|
|
|
# Create a prof ed course
|
|
course = CourseFactory.create()
|
|
CourseModeFactory(mode_slug="professional", course_id=course.id)
|
|
|
|
# Create an order for a prof ed course
|
|
url = reverse('verify_student_create_order')
|
|
params = {
|
|
'course_id': unicode(course.id)
|
|
}
|
|
response = self.client.post(url, params)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# Verify that the course ID and transaction type are included in "merchant-defined data"
|
|
data = json.loads(response.content)
|
|
self.assertEqual(data['merchant_defined_data1'], unicode(course.id))
|
|
self.assertEqual(data['merchant_defined_data2'], "professional")
|
|
|
|
def test_create_order_set_donation_amount(self):
|
|
# Verify the student so we don't need to submit photos
|
|
self._verify_student()
|
|
|
|
# Create an order
|
|
url = reverse('verify_student_create_order')
|
|
params = {
|
|
'course_id': unicode(self.course.id),
|
|
'contribution': '1.23'
|
|
}
|
|
self.client.post(url, params)
|
|
|
|
# Verify that the client's session contains the new donation amount
|
|
self.assertIn('donation_for_course', self.client.session)
|
|
self.assertIn(unicode(self.course.id), self.client.session['donation_for_course'])
|
|
|
|
actual_amount = self.client.session['donation_for_course'][unicode(self.course.id)]
|
|
expected_amount = decimal.Decimal('1.23')
|
|
self.assertEqual(actual_amount, expected_amount)
|
|
|
|
def _verify_student(self):
|
|
""" Simulate that the student's identity has already been verified. """
|
|
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
|
attempt.mark_ready()
|
|
attempt.submit()
|
|
attempt.approve()
|
|
|
|
|
|
class TestCreateOrderView(ModuleStoreTestCase):
|
|
"""
|
|
Tests for the create_order view of verified course enrollment process.
|
|
"""
|
|
# Minimum size valid image data
|
|
IMAGE_DATA = ','
|
|
|
|
def setUp(self):
|
|
super(TestCreateOrderView, self).setUp()
|
|
|
|
self.user = UserFactory.create(username="rusty", password="test")
|
|
self.client.login(username="rusty", password="test")
|
|
self.course_id = 'Robot/999/Test_Course'
|
|
self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course')
|
|
verified_mode = CourseMode(
|
|
course_id=SlashSeparatedCourseKey("Robot", "999", 'Test_Course'),
|
|
mode_slug="verified",
|
|
mode_display_name="Verified Certificate",
|
|
min_price=50
|
|
)
|
|
verified_mode.save()
|
|
course_mode_post_data = {
|
|
'certificate_mode': 'Select Certificate',
|
|
'contribution': 50,
|
|
'contribution-other-amt': '',
|
|
'explain': ''
|
|
}
|
|
self.client.post(
|
|
reverse("course_modes_choose", kwargs={'course_id': self.course_id}),
|
|
course_mode_post_data
|
|
)
|
|
|
|
def test_invalid_photos_data(self):
|
|
self._create_order(
|
|
50,
|
|
self.course_id,
|
|
face_image='',
|
|
photo_id_image='',
|
|
expect_success=False
|
|
)
|
|
|
|
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
|
def test_invalid_amount(self):
|
|
response = self._create_order(
|
|
'1.a',
|
|
self.course_id,
|
|
face_image=self.IMAGE_DATA,
|
|
photo_id_image=self.IMAGE_DATA,
|
|
expect_status_code=400
|
|
)
|
|
self.assertIn('Selected price is not valid number.', response.content)
|
|
|
|
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
|
def test_invalid_mode(self):
|
|
# Create a course that does not have a verified mode
|
|
course_id = 'Fake/999/Test_Course'
|
|
CourseFactory.create(org='Fake', number='999', display_name='Test Course')
|
|
response = self._create_order(
|
|
'50',
|
|
course_id,
|
|
face_image=self.IMAGE_DATA,
|
|
photo_id_image=self.IMAGE_DATA,
|
|
expect_status_code=400
|
|
)
|
|
self.assertIn('This course doesn\'t support verified certificates', response.content)
|
|
|
|
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
|
def test_create_order_fail_with_get(self):
|
|
create_order_post_data = {
|
|
'contribution': 50,
|
|
'course_id': self.course_id,
|
|
'face_image': self.IMAGE_DATA,
|
|
'photo_id_image': self.IMAGE_DATA,
|
|
}
|
|
|
|
# Use the wrong HTTP method
|
|
response = self.client.get(reverse('verify_student_create_order'), create_order_post_data)
|
|
self.assertEqual(response.status_code, 405)
|
|
|
|
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
|
def test_create_order_success(self):
|
|
response = self._create_order(
|
|
50,
|
|
self.course_id,
|
|
face_image=self.IMAGE_DATA,
|
|
photo_id_image=self.IMAGE_DATA
|
|
)
|
|
json_response = json.loads(response.content)
|
|
self.assertIsNotNone(json_response.get('orderNumber'))
|
|
|
|
# Verify that the order exists and is configured correctly
|
|
order = Order.objects.get(user=self.user)
|
|
self.assertEqual(order.status, 'paying')
|
|
item = CertificateItem.objects.get(order=order)
|
|
self.assertEqual(item.status, 'paying')
|
|
self.assertEqual(item.course_id, self.course.id)
|
|
self.assertEqual(item.mode, 'verified')
|
|
|
|
def _create_order(
|
|
self, contribution, course_id,
|
|
face_image=None,
|
|
photo_id_image=None,
|
|
expect_success=True,
|
|
expect_status_code=200
|
|
):
|
|
"""Create a new order.
|
|
|
|
Arguments:
|
|
contribution (int): The contribution amount.
|
|
course_id (CourseKey): The course to purchase.
|
|
|
|
Keyword Arguments:
|
|
face_image (string): Base-64 encoded image data
|
|
photo_id_image (string): Base-64 encoded image data
|
|
expect_success (bool): If True, verify that the response was successful.
|
|
expect_status_code (int): The expected HTTP status code
|
|
|
|
Returns:
|
|
HttpResponse
|
|
|
|
"""
|
|
url = reverse('verify_student_create_order')
|
|
data = {
|
|
'contribution': contribution,
|
|
'course_id': course_id
|
|
}
|
|
|
|
if face_image is not None:
|
|
data['face_image'] = face_image
|
|
if photo_id_image is not None:
|
|
data['photo_id_image'] = photo_id_image
|
|
|
|
response = self.client.post(url, data)
|
|
self.assertEqual(response.status_code, expect_status_code)
|
|
|
|
if expect_status_code == 200:
|
|
json_response = json.loads(response.content)
|
|
if expect_success:
|
|
self.assertTrue(json_response.get('success'))
|
|
else:
|
|
self.assertFalse(json_response.get('success'))
|
|
|
|
return response
|
|
|
|
|
|
@ddt.ddt
|
|
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
|
class TestSubmitPhotosForVerification(TestCase):
|
|
"""
|
|
Tests for submitting photos for verification.
|
|
"""
|
|
USERNAME = "test_user"
|
|
PASSWORD = "test_password"
|
|
IMAGE_DATA = "abcd,1234"
|
|
FULL_NAME = u"Ḟüḷḷ Ṅäṁë"
|
|
|
|
def setUp(self):
|
|
super(TestSubmitPhotosForVerification, self).setUp()
|
|
self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD)
|
|
result = self.client.login(username=self.USERNAME, password=self.PASSWORD)
|
|
self.assertTrue(result, msg="Could not log in")
|
|
|
|
def test_submit_photos(self):
|
|
# Submit the photos
|
|
self._submit_photos(
|
|
face_image=self.IMAGE_DATA,
|
|
photo_id_image=self.IMAGE_DATA
|
|
)
|
|
|
|
# Verify that the attempt is created in the database
|
|
attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user)
|
|
self.assertEqual(attempt.status, "submitted")
|
|
|
|
# Verify that the user's name wasn't changed
|
|
self._assert_full_name(self.user.profile.name)
|
|
|
|
def test_submit_photos_and_change_name(self):
|
|
# Submit the photos, along with a name change
|
|
self._submit_photos(
|
|
face_image=self.IMAGE_DATA,
|
|
photo_id_image=self.IMAGE_DATA,
|
|
full_name=self.FULL_NAME
|
|
)
|
|
|
|
# Check that the user's name was changed in the database
|
|
self._assert_full_name(self.FULL_NAME)
|
|
|
|
@ddt.data('face_image', 'photo_id_image')
|
|
def test_invalid_image_data(self, invalid_param):
|
|
params = {
|
|
'face_image': self.IMAGE_DATA,
|
|
'photo_id_image': self.IMAGE_DATA
|
|
}
|
|
params[invalid_param] = ""
|
|
response = self._submit_photos(expected_status_code=400, **params)
|
|
self.assertEqual(response.content, "Image data is not valid.")
|
|
|
|
def test_invalid_name(self):
|
|
response = self._submit_photos(
|
|
face_image=self.IMAGE_DATA,
|
|
photo_id_image=self.IMAGE_DATA,
|
|
full_name="a",
|
|
expected_status_code=400
|
|
)
|
|
self.assertEqual(response.content, "Name must be at least 2 characters long.")
|
|
|
|
@ddt.data('face_image', 'photo_id_image')
|
|
def test_missing_required_params(self, missing_param):
|
|
params = {
|
|
'face_image': self.IMAGE_DATA,
|
|
'photo_id_image': self.IMAGE_DATA
|
|
}
|
|
del params[missing_param]
|
|
response = self._submit_photos(expected_status_code=400, **params)
|
|
self.assertEqual(
|
|
response.content,
|
|
"Missing required parameters: {missing}".format(missing=missing_param)
|
|
)
|
|
|
|
def _submit_photos(self, face_image=None, photo_id_image=None, full_name=None, expected_status_code=200):
|
|
"""Submit photos for verification.
|
|
|
|
Keyword Arguments:
|
|
face_image (str): The base-64 encoded face image data.
|
|
photo_id_image (str): The base-64 encoded ID image data.
|
|
full_name (unicode): The full name of the user, if the user is changing it.
|
|
expected_status_code (int): The expected response status code.
|
|
|
|
Returns:
|
|
HttpResponse
|
|
|
|
"""
|
|
url = reverse("verify_student_submit_photos")
|
|
params = {}
|
|
|
|
if face_image is not None:
|
|
params['face_image'] = face_image
|
|
|
|
if photo_id_image is not None:
|
|
params['photo_id_image'] = photo_id_image
|
|
|
|
if full_name is not None:
|
|
params['full_name'] = full_name
|
|
|
|
response = self.client.post(url, params)
|
|
self.assertEqual(response.status_code, expected_status_code)
|
|
|
|
if expected_status_code == 200:
|
|
# Verify that photo submission confirmation email was sent
|
|
self.assertEqual(len(mail.outbox), 1)
|
|
self.assertEqual("Verification photos received", mail.outbox[0].subject)
|
|
else:
|
|
# Verify that photo submission confirmation email was not sent
|
|
self.assertEqual(len(mail.outbox), 0)
|
|
|
|
return response
|
|
|
|
def _assert_full_name(self, full_name):
|
|
"""Check the user's full name.
|
|
|
|
Arguments:
|
|
full_name (unicode): The user's full name.
|
|
|
|
Raises:
|
|
AssertionError
|
|
|
|
"""
|
|
info = profile_api.profile_info(self.user.username)
|
|
self.assertEqual(info['full_name'], full_name)
|
|
|
|
|
|
class TestPhotoVerificationResultsCallback(ModuleStoreTestCase):
|
|
"""
|
|
Tests for the results_callback view.
|
|
"""
|
|
def setUp(self):
|
|
super(TestPhotoVerificationResultsCallback, self).setUp()
|
|
|
|
self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course')
|
|
self.course_id = self.course.id
|
|
self.user = UserFactory.create()
|
|
self.attempt = SoftwareSecurePhotoVerification(
|
|
status="submitted",
|
|
user=self.user
|
|
)
|
|
self.attempt.save()
|
|
self.receipt_id = self.attempt.receipt_id
|
|
self.client = Client()
|
|
|
|
def mocked_has_valid_signature(method, headers_dict, body_dict, access_key, secret_key): # pylint: disable=no-self-argument, unused-argument
|
|
"""
|
|
Used as a side effect when mocking `verify_student.ssencrypt.has_valid_signature`.
|
|
"""
|
|
return True
|
|
|
|
def test_invalid_json(self):
|
|
"""
|
|
Test for invalid json being posted by software secure.
|
|
"""
|
|
data = {"Testing invalid"}
|
|
response = self.client.post(
|
|
reverse('verify_student_results_callback'),
|
|
data=data,
|
|
content_type='application/json',
|
|
HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB: testing',
|
|
HTTP_DATE='testdate'
|
|
)
|
|
self.assertIn('Invalid JSON', response.content)
|
|
self.assertEqual(response.status_code, 400)
|
|
|
|
def test_invalid_dict(self):
|
|
"""
|
|
Test for invalid dictionary being posted by software secure.
|
|
"""
|
|
data = '"\\"Test\\tTesting"'
|
|
response = self.client.post(
|
|
reverse('verify_student_results_callback'),
|
|
data=data,
|
|
content_type='application/json',
|
|
HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing',
|
|
HTTP_DATE='testdate'
|
|
)
|
|
self.assertIn('JSON should be dict', response.content)
|
|
self.assertEqual(response.status_code, 400)
|
|
|
|
@mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
|
|
def test_invalid_access_key(self):
|
|
"""
|
|
Test for invalid access key.
|
|
"""
|
|
data = {
|
|
"EdX-ID": self.receipt_id,
|
|
"Result": "Testing",
|
|
"Reason": "Testing",
|
|
"MessageType": "Testing"
|
|
}
|
|
json_data = json.dumps(data)
|
|
response = self.client.post(
|
|
reverse('verify_student_results_callback'),
|
|
data=json_data,
|
|
content_type='application/json',
|
|
HTTP_AUTHORIZATION='test testing:testing',
|
|
HTTP_DATE='testdate'
|
|
)
|
|
self.assertIn('Access key invalid', response.content)
|
|
self.assertEqual(response.status_code, 400)
|
|
|
|
@mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
|
|
def test_wrong_edx_id(self):
|
|
"""
|
|
Test for wrong id of Software secure verification attempt.
|
|
"""
|
|
data = {
|
|
"EdX-ID": "Invalid-Id",
|
|
"Result": "Testing",
|
|
"Reason": "Testing",
|
|
"MessageType": "Testing"
|
|
}
|
|
json_data = json.dumps(data)
|
|
response = self.client.post(
|
|
reverse('verify_student_results_callback'),
|
|
data=json_data,
|
|
content_type='application/json',
|
|
HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing',
|
|
HTTP_DATE='testdate'
|
|
)
|
|
self.assertIn('edX ID Invalid-Id not found', response.content)
|
|
self.assertEqual(response.status_code, 400)
|
|
|
|
@mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
|
|
def test_pass_result(self):
|
|
"""
|
|
Test for verification passed.
|
|
"""
|
|
data = {
|
|
"EdX-ID": self.receipt_id,
|
|
"Result": "PASS",
|
|
"Reason": "",
|
|
"MessageType": "You have been verified."
|
|
}
|
|
json_data = json.dumps(data)
|
|
response = self.client.post(
|
|
reverse('verify_student_results_callback'), data=json_data,
|
|
content_type='application/json',
|
|
HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing',
|
|
HTTP_DATE='testdate'
|
|
)
|
|
attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id)
|
|
self.assertEqual(attempt.status, u'approved')
|
|
self.assertEquals(response.content, 'OK!')
|
|
|
|
@mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
|
|
def test_fail_result(self):
|
|
"""
|
|
Test for failed verification.
|
|
"""
|
|
data = {
|
|
"EdX-ID": self.receipt_id,
|
|
"Result": 'FAIL',
|
|
"Reason": 'Invalid photo',
|
|
"MessageType": 'Your photo doesn\'t meet standards.'
|
|
}
|
|
json_data = json.dumps(data)
|
|
response = self.client.post(
|
|
reverse('verify_student_results_callback'),
|
|
data=json_data,
|
|
content_type='application/json',
|
|
HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing',
|
|
HTTP_DATE='testdate'
|
|
)
|
|
attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id)
|
|
self.assertEqual(attempt.status, u'denied')
|
|
self.assertEqual(attempt.error_code, u'Your photo doesn\'t meet standards.')
|
|
self.assertEqual(attempt.error_msg, u'"Invalid photo"')
|
|
self.assertEquals(response.content, 'OK!')
|
|
|
|
@mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
|
|
def test_system_fail_result(self):
|
|
"""
|
|
Test for software secure result system failure.
|
|
"""
|
|
data = {"EdX-ID": self.receipt_id,
|
|
"Result": 'SYSTEM FAIL',
|
|
"Reason": 'Memory overflow',
|
|
"MessageType": 'You must retry the verification.'}
|
|
json_data = json.dumps(data)
|
|
response = self.client.post(
|
|
reverse('verify_student_results_callback'),
|
|
data=json_data,
|
|
content_type='application/json',
|
|
HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing',
|
|
HTTP_DATE='testdate'
|
|
)
|
|
attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id)
|
|
self.assertEqual(attempt.status, u'must_retry')
|
|
self.assertEqual(attempt.error_code, u'You must retry the verification.')
|
|
self.assertEqual(attempt.error_msg, u'"Memory overflow"')
|
|
self.assertEquals(response.content, 'OK!')
|
|
|
|
@mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
|
|
def test_unknown_result(self):
|
|
"""
|
|
test for unknown software secure result
|
|
"""
|
|
data = {
|
|
"EdX-ID": self.receipt_id,
|
|
"Result": 'Unknown',
|
|
"Reason": 'Unknown reason',
|
|
"MessageType": 'Unknown message'
|
|
}
|
|
json_data = json.dumps(data)
|
|
response = self.client.post(
|
|
reverse('verify_student_results_callback'),
|
|
data=json_data,
|
|
content_type='application/json',
|
|
HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing',
|
|
HTTP_DATE='testdate'
|
|
)
|
|
self.assertIn('Result Unknown not understood', response.content)
|
|
|
|
@mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature))
|
|
def test_reverification(self):
|
|
"""
|
|
Test software secure result for reverification window.
|
|
"""
|
|
data = {
|
|
"EdX-ID": self.receipt_id,
|
|
"Result": "PASS",
|
|
"Reason": "",
|
|
"MessageType": "You have been verified."
|
|
}
|
|
window = MidcourseReverificationWindowFactory(course_id=self.course_id)
|
|
self.attempt.window = window
|
|
self.attempt.save()
|
|
json_data = json.dumps(data)
|
|
self.assertEqual(CourseEnrollment.objects.filter(course_id=self.course_id).count(), 0)
|
|
response = self.client.post(
|
|
reverse('verify_student_results_callback'),
|
|
data=json_data,
|
|
content_type='application/json',
|
|
HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing',
|
|
HTTP_DATE='testdate'
|
|
)
|
|
self.assertEquals(response.content, 'OK!')
|
|
self.assertIsNotNone(CourseEnrollment.objects.get(course_id=self.course_id))
|
|
|
|
|
|
class TestReverifyView(ModuleStoreTestCase):
|
|
"""
|
|
Tests for the reverification views
|
|
"""
|
|
def setUp(self):
|
|
super(TestReverifyView, self).setUp()
|
|
|
|
self.user = UserFactory.create(username="rusty", password="test")
|
|
self.user.profile.name = u"Røøsty Bøøgins"
|
|
self.user.profile.save()
|
|
self.client.login(username="rusty", password="test")
|
|
self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course')
|
|
self.course_key = self.course.id
|
|
|
|
@patch('verify_student.views.render_to_response', render_mock)
|
|
def test_reverify_get(self):
|
|
url = reverse('verify_student_reverify')
|
|
response = self.client.get(url)
|
|
self.assertEquals(response.status_code, 200)
|
|
((_template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence
|
|
self.assertFalse(context['error'])
|
|
|
|
@patch('verify_student.views.render_to_response', render_mock)
|
|
def test_reverify_post_failure(self):
|
|
url = reverse('verify_student_reverify')
|
|
response = self.client.post(url, {'face_image': '',
|
|
'photo_id_image': ''})
|
|
self.assertEquals(response.status_code, 200)
|
|
((template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence
|
|
self.assertIn('photo_reverification', template)
|
|
self.assertTrue(context['error'])
|
|
|
|
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
|
def test_reverify_post_success(self):
|
|
url = reverse('verify_student_reverify')
|
|
response = self.client.post(url, {'face_image': ',',
|
|
'photo_id_image': ','})
|
|
self.assertEquals(response.status_code, 302)
|
|
try:
|
|
verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user)
|
|
self.assertIsNotNone(verification_attempt)
|
|
except ObjectDoesNotExist:
|
|
self.fail('No verification object generated')
|
|
((template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence
|
|
self.assertIn('photo_reverification', template)
|
|
self.assertTrue(context['error'])
|
|
|
|
|
|
class TestMidCourseReverifyView(ModuleStoreTestCase):
|
|
"""
|
|
Tests for the midcourse reverification views.
|
|
"""
|
|
def setUp(self):
|
|
super(TestMidCourseReverifyView, self).setUp()
|
|
|
|
self.user = UserFactory.create(username="rusty", password="test")
|
|
self.client.login(username="rusty", password="test")
|
|
self.course_key = SlashSeparatedCourseKey("Robot", "999", "Test_Course")
|
|
CourseFactory.create(org='Robot', number='999', display_name='Test Course')
|
|
|
|
patcher = patch('student.models.tracker')
|
|
self.mock_tracker = patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
@patch('verify_student.views.render_to_response', render_mock)
|
|
def test_midcourse_reverify_get(self):
|
|
url = reverse('verify_student_midcourse_reverify',
|
|
kwargs={"course_id": self.course_key.to_deprecated_string()})
|
|
response = self.client.get(url)
|
|
|
|
self.mock_tracker.emit.assert_any_call( # pylint: disable=maybe-no-member
|
|
'edx.course.enrollment.mode_changed',
|
|
{
|
|
'user_id': self.user.id,
|
|
'course_id': self.course_key.to_deprecated_string(),
|
|
'mode': "verified",
|
|
}
|
|
)
|
|
|
|
# Check that user entering the reverify flow was logged, and that it was the last call
|
|
self.mock_tracker.emit.assert_called_with( # pylint: disable=maybe-no-member
|
|
'edx.course.enrollment.reverify.started',
|
|
{
|
|
'user_id': self.user.id,
|
|
'course_id': self.course_key.to_deprecated_string(),
|
|
'mode': "verified",
|
|
}
|
|
)
|
|
|
|
self.assertTrue(self.mock_tracker.emit.call_count, 2) # pylint: disable=no-member
|
|
|
|
self.mock_tracker.emit.reset_mock() # pylint: disable=no-member
|
|
|
|
self.assertEquals(response.status_code, 200)
|
|
((_template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence
|
|
self.assertFalse(context['error'])
|
|
|
|
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
|
def test_midcourse_reverify_post_success(self):
|
|
window = MidcourseReverificationWindowFactory(course_id=self.course_key)
|
|
url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_key.to_deprecated_string()})
|
|
|
|
response = self.client.post(url, {'face_image': ','})
|
|
|
|
self.mock_tracker.emit.assert_any_call( # pylint: disable=maybe-no-member
|
|
'edx.course.enrollment.mode_changed',
|
|
{
|
|
'user_id': self.user.id,
|
|
'course_id': self.course_key.to_deprecated_string(),
|
|
'mode': "verified",
|
|
}
|
|
)
|
|
|
|
# Check that submission event was logged, and that it was the last call
|
|
self.mock_tracker.emit.assert_called_with( # pylint: disable=maybe-no-member
|
|
'edx.course.enrollment.reverify.submitted',
|
|
{
|
|
'user_id': self.user.id,
|
|
'course_id': self.course_key.to_deprecated_string(),
|
|
'mode': "verified",
|
|
}
|
|
)
|
|
|
|
self.assertTrue(self.mock_tracker.emit.call_count, 2) # pylint: disable=no-member
|
|
|
|
self.mock_tracker.emit.reset_mock() # pylint: disable=no-member
|
|
|
|
self.assertEquals(response.status_code, 302)
|
|
try:
|
|
verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user, window=window)
|
|
self.assertIsNotNone(verification_attempt)
|
|
except ObjectDoesNotExist:
|
|
self.fail('No verification object generated')
|
|
|
|
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
|
def test_midcourse_reverify_post_failure_expired_window(self):
|
|
window = MidcourseReverificationWindowFactory(
|
|
course_id=self.course_key,
|
|
start_date=datetime.now(pytz.UTC) - timedelta(days=100),
|
|
end_date=datetime.now(pytz.UTC) - timedelta(days=50),
|
|
)
|
|
url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_key.to_deprecated_string()})
|
|
response = self.client.post(url, {'face_image': ','})
|
|
self.assertEquals(response.status_code, 302)
|
|
with self.assertRaises(ObjectDoesNotExist):
|
|
SoftwareSecurePhotoVerification.objects.get(user=self.user, window=window)
|
|
|
|
@patch('verify_student.views.render_to_response', render_mock)
|
|
def test_midcourse_reverify_dash(self):
|
|
url = reverse('verify_student_midcourse_reverify_dash')
|
|
response = self.client.get(url)
|
|
# not enrolled in any courses
|
|
self.assertEquals(response.status_code, 200)
|
|
|
|
enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_key)
|
|
enrollment.update_enrollment(mode="verified", is_active=True)
|
|
MidcourseReverificationWindowFactory(course_id=self.course_key)
|
|
response = self.client.get(url)
|
|
# enrolled in a verified course, and the window is open
|
|
self.assertEquals(response.status_code, 200)
|
|
|
|
@patch('verify_student.views.render_to_response', render_mock)
|
|
def test_midcourse_reverify_invalid_course_id(self):
|
|
# if course id is invalid return 400
|
|
invalid_course_key = CourseLocator('edx', 'not', 'valid')
|
|
url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': unicode(invalid_course_key)})
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
|
|
class TestReverificationBanner(ModuleStoreTestCase):
|
|
"""
|
|
Tests for toggling the "midcourse reverification failed" banner off.
|
|
"""
|
|
|
|
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
|
def setUp(self):
|
|
super(TestReverificationBanner, self).setUp()
|
|
|
|
self.user = UserFactory.create(username="rusty", password="test")
|
|
self.client.login(username="rusty", password="test")
|
|
self.course_id = 'Robot/999/Test_Course'
|
|
CourseFactory.create(org='Robot', number='999', display_name=u'Test Course é')
|
|
self.window = MidcourseReverificationWindowFactory(course_id=self.course_id)
|
|
url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_id})
|
|
self.client.post(url, {'face_image': ','})
|
|
photo_verification = SoftwareSecurePhotoVerification.objects.get(user=self.user, window=self.window)
|
|
photo_verification.status = 'denied'
|
|
photo_verification.save()
|
|
|
|
def test_banner_display_off(self):
|
|
self.client.post(reverse('verify_student_toggle_failed_banner_off'))
|
|
photo_verification = SoftwareSecurePhotoVerification.objects.get(user=self.user, window=self.window)
|
|
self.assertFalse(photo_verification.display)
|