Files
edx-platform/common/djangoapps/student/tests/test_verification_status.py
Calen Pennington b353ed2ea2 Better support specifying of modulestore configuration in test cases
The existing pattern of using `override_settings(MODULESTORE=...)` prevented
us from having more than one layer of subclassing in modulestore tests.

In a structure like:

    @override_settings(MODULESTORE=store_a)
    class BaseTestCase(ModuleStoreTestCase):
        def setUp(self):
            # use store

    @override_settings(MODULESTORE=store_b)
    class ChildTestCase(BaseTestCase):
        def setUp(self):
            # use store

In this case, the store actions performed in `BaseTestCase` on behalf of
`ChildTestCase` would still use `store_a`, even though the `ChildTestCase`
had specified to use `store_b`. This is because the `override_settings`
decorator would be the innermost wrapper around the `BaseTestCase.setUp` method,
no matter what `ChildTestCase` does.

To remedy this, we move the call to `override_settings` into the
`ModuleStoreTestCase.setUp` method, and use a cleanup to remove the override.
Subclasses can just defined the `MODULESTORE` class attribute to specify which
modulestore to use _for the entire `setUp` chain_.

[PLAT-419]
2015-02-04 09:09:14 -05:00

308 lines
12 KiB
Python

"""Tests for per-course verification status on the dashboard. """
from datetime import datetime, timedelta
import unittest
import ddt
from mock import patch
from pytz import UTC
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from django.conf import settings
from student.helpers import (
VERIFY_STATUS_NEED_TO_VERIFY,
VERIFY_STATUS_SUBMITTED,
VERIFY_STATUS_APPROVED,
VERIFY_STATUS_MISSED_DEADLINE
)
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from course_modes.tests.factories import CourseModeFactory
from verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=F0401
from util.testing import UrlResetMixin
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@ddt.ddt
class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
"""Tests for per-course verification status on the dashboard. """
PAST = datetime.now(UTC) - timedelta(days=5)
FUTURE = datetime.now(UTC) + timedelta(days=5)
def setUp(self):
# Invoke UrlResetMixin
super(TestCourseVerificationStatus, self).setUp('verify_student.urls')
self.user = UserFactory(password="edx")
self.course = CourseFactory.create()
success = self.client.login(username=self.user.username, password="edx")
self.assertTrue(success, msg="Did not log in successfully")
# Use the URL with the querystring param to put the user
# in the experimental track.
# TODO (ECOM-188): Once the A/B test of decoupling verified / payment
# completes, we can remove the querystring param.
self.dashboard_url = reverse('dashboard') + '?separate-verified=1'
def test_enrolled_as_non_verified(self):
self._setup_mode_and_enrollment(None, "honor")
# Expect that the course appears on the dashboard
# without any verification messaging
self._assert_course_verification_status(None)
def test_no_verified_mode_available(self):
# Enroll the student in a verified mode, but don't
# create any verified course mode.
# This won't happen unless someone deletes a course mode,
# but if so, make sure we handle it gracefully.
CourseEnrollmentFactory(
course_id=self.course.id,
user=self.user,
mode="verified"
)
# The default course has no verified mode,
# so no verification status should be displayed
self._assert_course_verification_status(None)
def test_need_to_verify_no_expiration(self):
self._setup_mode_and_enrollment(None, "verified")
# Since the student has not submitted a photo verification,
# the student should see a "need to verify" message
self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY)
# Start the photo verification process, but do not submit
# Since we haven't submitted the verification, we should still
# see the "need to verify" message
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY)
# Upload images, but don't submit to the verification service
# We should still need to verify
attempt.mark_ready()
self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY)
def test_need_to_verify_expiration(self):
self._setup_mode_and_enrollment(self.FUTURE, "verified")
response = self.client.get(self.dashboard_url)
self.assertContains(response, self.BANNER_ALT_MESSAGES[VERIFY_STATUS_NEED_TO_VERIFY])
self.assertContains(response, "You only have 4 days left to verify for this course.")
@ddt.data(None, FUTURE)
def test_waiting_approval(self, expiration):
self._setup_mode_and_enrollment(expiration, "verified")
# The student has submitted a photo verification
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
# Now the student should see a "verification submitted" message
self._assert_course_verification_status(VERIFY_STATUS_SUBMITTED)
@ddt.data(None, FUTURE)
def test_fully_verified(self, expiration):
self._setup_mode_and_enrollment(expiration, "verified")
# The student has an approved verification
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
attempt.approve()
# Expect that the successfully verified message is shown
self._assert_course_verification_status(VERIFY_STATUS_APPROVED)
# Check that the "verification good until" date is displayed
response = self.client.get(self.dashboard_url)
self.assertContains(response, attempt.expiration_datetime.strftime("%m/%d/%Y"))
def test_missed_verification_deadline(self):
# Expiration date in the past
self._setup_mode_and_enrollment(self.PAST, "verified")
# The student does NOT have an approved verification
# so the status should show that the student missed the deadline.
self._assert_course_verification_status(VERIFY_STATUS_MISSED_DEADLINE)
def test_missed_verification_deadline_verification_was_expired(self):
# Expiration date in the past
self._setup_mode_and_enrollment(self.PAST, "verified")
# Create a verification, but the expiration date of the verification
# occurred before the deadline.
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
attempt.approve()
attempt.created_at = self.PAST - timedelta(days=900)
attempt.save()
# The student didn't have an approved verification at the deadline,
# so we should show that the student missed the deadline.
self._assert_course_verification_status(VERIFY_STATUS_MISSED_DEADLINE)
def test_missed_verification_deadline_but_later_verified(self):
# Expiration date in the past
self._setup_mode_and_enrollment(self.PAST, "verified")
# Successfully verify, but after the deadline has already passed
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
attempt.approve()
attempt.created_at = self.PAST - timedelta(days=900)
attempt.save()
# The student didn't have an approved verification at the deadline,
# so we should show that the student missed the deadline.
self._assert_course_verification_status(VERIFY_STATUS_MISSED_DEADLINE)
def test_verification_denied(self):
# Expiration date in the future
self._setup_mode_and_enrollment(self.FUTURE, "verified")
# Create a verification with the specified status
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
attempt.deny("Not valid!")
# Since this is not a status we handle, don't display any
# messaging relating to verification
self._assert_course_verification_status(None)
def test_verification_error(self):
# Expiration date in the future
self._setup_mode_and_enrollment(self.FUTURE, "verified")
# Create a verification with the specified status
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.status = "must_retry"
attempt.system_error("Error!")
# Since this is not a status we handle, don't display any
# messaging relating to verification
self._assert_course_verification_status(None)
def test_verification_will_expire_by_deadline(self):
# Expiration date in the future
self._setup_mode_and_enrollment(self.FUTURE, "verified")
# Create a verification attempt that:
# 1) Is current (submitted in the last year)
# 2) Will expire by the deadline for the course
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
# This attempt will expire tomorrow, before the course deadline
attempt.created_at = attempt.created_at - timedelta(days=364)
attempt.save()
# Expect that the "verify now" message is hidden
# (since the user isn't allowed to submit another attempt while
# a verification is active).
self._assert_course_verification_status(None)
def _setup_mode_and_enrollment(self, deadline, enrollment_mode):
"""Create a course mode and enrollment.
Arguments:
deadline (datetime): The deadline for submitting your verification.
enrollment_mode (str): The mode of the enrollment.
"""
CourseModeFactory(
course_id=self.course.id,
mode_slug="verified",
expiration_datetime=deadline
)
CourseEnrollmentFactory(
course_id=self.course.id,
user=self.user,
mode=enrollment_mode
)
BANNER_ALT_MESSAGES = {
None: "Honor",
VERIFY_STATUS_NEED_TO_VERIFY: "ID verification pending",
VERIFY_STATUS_SUBMITTED: "ID verification pending",
VERIFY_STATUS_APPROVED: "ID Verified Ribbon/Badge",
VERIFY_STATUS_MISSED_DEADLINE: "Honor"
}
NOTIFICATION_MESSAGES = {
VERIFY_STATUS_NEED_TO_VERIFY: [
"You still need to verify for this course.",
"Verification not yet complete"
],
VERIFY_STATUS_SUBMITTED: ["Thanks for your patience as we process your request."],
VERIFY_STATUS_APPROVED: ["You have already verified your ID!"],
}
MODE_CLASSES = {
None: "honor",
VERIFY_STATUS_NEED_TO_VERIFY: "verified",
VERIFY_STATUS_SUBMITTED: "verified",
VERIFY_STATUS_APPROVED: "verified",
VERIFY_STATUS_MISSED_DEADLINE: "honor"
}
def _assert_course_verification_status(self, status):
"""Check whether the specified verification status is shown on the dashboard.
Arguments:
status (str): One of the verification status constants.
If None, check that *none* of the statuses are displayed.
Raises:
AssertionError
"""
response = self.client.get(self.dashboard_url)
# Sanity check: verify that the course is on the page
self.assertContains(response, unicode(self.course.id))
# Verify that the correct banner is rendered on the dashboard
self.assertContains(response, self.BANNER_ALT_MESSAGES[status])
# Verify that the correct banner color is rendered
self.assertContains(
response,
"<article class=\"course {}\">".format(self.MODE_CLASSES[status])
)
# Verify that the correct copy is rendered on the dashboard
if status is not None:
if status in self.NOTIFICATION_MESSAGES:
# Different states might have different messaging
# so in some cases we check several possibilities
# and fail if none of these are found.
found_msg = False
for message in self.NOTIFICATION_MESSAGES[status]:
if message in response.content:
found_msg = True
break
fail_msg = "Could not find any of these messages: {expected}".format(
expected=self.NOTIFICATION_MESSAGES[status]
)
self.assertTrue(found_msg, msg=fail_msg)
else:
# Combine all possible messages into a single list
all_messages = []
for msg_group in self.NOTIFICATION_MESSAGES.values():
all_messages.extend(msg_group)
# Verify that none of the messages are displayed
for msg in all_messages:
self.assertNotContains(response, msg)