Files
edx-platform/common/djangoapps/student/tests/test_verification_status.py

398 lines
16 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 nose.plugins.attrib import attr
from pytz import UTC
from django.core.urlresolvers import reverse
from django.conf import settings
from django.test import override_settings
from student.helpers import (
VERIFY_STATUS_NEED_TO_VERIFY,
VERIFY_STATUS_SUBMITTED,
VERIFY_STATUS_RESUBMITTED,
VERIFY_STATUS_APPROVED,
VERIFY_STATUS_MISSED_DEADLINE,
VERIFY_STATUS_NEED_TO_REVERIFY
)
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 lms.djangoapps.verify_student.models import VerificationDeadline, SoftwareSecurePhotoVerification
from util.testing import UrlResetMixin
@attr(shard=3)
@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)
URLCONF_MODULES = ['verify_student.urls']
def setUp(self):
# Invoke UrlResetMixin
super(TestCourseVerificationStatus, self).setUp()
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")
self.dashboard_url = reverse('dashboard')
def test_enrolled_as_non_verified(self):
self._setup_mode_and_enrollment(None, "audit")
# 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"
)
# Continue to show the student as needing to verify.
# The student is enrolled as verified, so we might as well let them
# complete verification. We'd need to change their enrollment mode
# anyway to ensure that the student is issued the correct kind of certificate.
self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY)
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)
@override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10})
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()
attempt.approve()
attempt.save()
# Verify that learner can submit photos if verification is set to expire soon.
self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_REVERIFY)
@override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10})
def test_reverification_submitted_with_current_approved_verificaiton(self):
# Expiration date in the future
self._setup_mode_and_enrollment(self.FUTURE, "verified")
# Create a verification attempt that is approved but expiring soon
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
attempt.approve()
attempt.save()
# Verify that learner can submit photos if verification is set to expire soon.
self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_REVERIFY)
# Submit photos for reverification
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
# Expect that learner has submitted photos for reverfication and his/her
# previous verification is set to expired soon.
self._assert_course_verification_status(VERIFY_STATUS_RESUBMITTED)
def test_verification_occurred_after_deadline(self):
# Expiration date in the past
self._setup_mode_and_enrollment(self.PAST, "verified")
# The deadline has passed, and we've asked the student
# to reverify (through the support team).
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
# Expect that the user's displayed enrollment mode is verified.
self._assert_course_verification_status(VERIFY_STATUS_APPROVED)
def test_with_two_verifications(self):
# checking if a user has two verification and but most recent verification course deadline is expired
self._setup_mode_and_enrollment(self.FUTURE, "verified")
# The student has an approved verification
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
attempt.approve()
# Making created at to previous date to differentiate with 2nd attempt.
attempt.created_at = datetime.now(UTC) - timedelta(days=1)
attempt.save()
# 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"))
# Adding another verification with different course.
# Its created_at is greater than course deadline.
course2 = CourseFactory.create()
CourseModeFactory.create(
course_id=course2.id,
mode_slug="verified",
expiration_datetime=self.PAST
)
CourseEnrollmentFactory(
course_id=course2.id,
user=self.user,
mode="verified"
)
# The student has an approved verification
attempt2 = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt2.mark_ready()
attempt2.submit()
attempt2.approve()
attempt2.save()
# Mark the attemp2 as approved so its date will appear on dasboard.
self._assert_course_verification_status(VERIFY_STATUS_APPROVED)
response2 = self.client.get(self.dashboard_url)
self.assertContains(response2, attempt2.expiration_datetime.strftime("%m/%d/%Y"))
self.assertEqual(response2.content.count(attempt2.expiration_datetime.strftime("%m/%d/%Y")), 2)
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.create(
course_id=self.course.id,
mode_slug="verified",
expiration_datetime=deadline
)
CourseEnrollmentFactory(
course_id=self.course.id,
user=self.user,
mode=enrollment_mode
)
VerificationDeadline.set_deadline(self.course.id, deadline)
BANNER_ALT_MESSAGES = {
VERIFY_STATUS_NEED_TO_VERIFY: "ID verification pending",
VERIFY_STATUS_SUBMITTED: "ID verification pending",
VERIFY_STATUS_APPROVED: "ID Verified Ribbon/Badge",
}
NOTIFICATION_MESSAGES = {
VERIFY_STATUS_NEED_TO_VERIFY: [
"You still need to verify for this course.",
"Verification not yet complete"
],
VERIFY_STATUS_SUBMITTED: ["You have submitted your verification information."],
VERIFY_STATUS_RESUBMITTED: ["You have submitted your reverification information."],
VERIFY_STATUS_APPROVED: ["You have successfully verified your ID with edX"],
VERIFY_STATUS_NEED_TO_REVERIFY: ["Your current verification will expire soon."]
}
MODE_CLASSES = {
None: "audit",
VERIFY_STATUS_NEED_TO_VERIFY: "verified",
VERIFY_STATUS_SUBMITTED: "verified",
VERIFY_STATUS_APPROVED: "verified",
VERIFY_STATUS_MISSED_DEADLINE: "audit",
VERIFY_STATUS_NEED_TO_REVERIFY: "audit",
VERIFY_STATUS_RESUBMITTED: "audit"
}
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
alt_text = self.BANNER_ALT_MESSAGES.get(status)
if alt_text:
self.assertContains(response, alt_text)
# 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)