diff --git a/cms/envs/common.py b/cms/envs/common.py index cb8d7ad115..7b390d9982 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1897,12 +1897,6 @@ derived('HELP_TOKENS_LANGUAGE_CODE', 'HELP_TOKENS_VERSION') RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5 RETRY_ACTIVATION_EMAIL_TIMEOUT = 0.5 -# Software Secure request retry settings -# Time in seconds before a retry of the task should be 60 mints. -SOFTWARE_SECURE_REQUEST_RETRY_DELAY = 60 * 60 -# Maximum of 6 retries before giving up. -SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS = 6 - ############## DJANGO-USER-TASKS ############## # How long until database records about the outcome of a task and its artifacts get deleted? @@ -1955,8 +1949,6 @@ RECALCULATE_GRADES_ROUTING_KEY = DEFAULT_PRIORITY_QUEUE # Queue to use for updating grades due to grading policy change POLICY_CHANGE_GRADES_ROUTING_KEY = 'edx.lms.core.default' -SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = 'edx.lms.core.default' - # Rate limit for regrading tasks that a grading policy change can kick off POLICY_CHANGE_TASK_RATE_LIMIT = '300/h' diff --git a/cms/envs/production.py b/cms/envs/production.py index 9614ccbfb7..0b5302de7e 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -481,11 +481,6 @@ CELERY_QUEUES.update( # Queue to use for updating grades due to grading policy change POLICY_CHANGE_GRADES_ROUTING_KEY = ENV_TOKENS.get('POLICY_CHANGE_GRADES_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) -SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = ENV_TOKENS.get( - 'SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY', - HIGH_PRIORITY_QUEUE -) - # Rate limit for regrading tasks that a grading policy change can kick off POLICY_CHANGE_TASK_RATE_LIMIT = ENV_TOKENS.get('POLICY_CHANGE_TASK_RATE_LIMIT', POLICY_CHANGE_TASK_RATE_LIMIT) diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index c5953682ce..220927d02b 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -32,7 +32,7 @@ from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory from lms.djangoapps.certificates.models import CertificateStatuses # pylint: disable=import-error from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error -from lms.djangoapps.verify_student.tests import TestVerificationBase +from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory, ProgramFactory, generate_course_run_key from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin @@ -291,7 +291,7 @@ class CourseEndingTest(TestCase): @ddt.ddt -class DashboardTest(ModuleStoreTestCase, TestVerificationBase): +class DashboardTest(ModuleStoreTestCase): """ Tests for dashboard utility functions """ @@ -314,7 +314,9 @@ class DashboardTest(ModuleStoreTestCase, TestVerificationBase): if mode == 'verified': # Simulate a successful verification attempt - attempt = self.create_and_submit_attempt_for_user(self.user) + attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) + attempt.mark_ready() + attempt.submit() attempt.approve() response = self.client.get(reverse('dashboard')) @@ -349,7 +351,9 @@ class DashboardTest(ModuleStoreTestCase, TestVerificationBase): if mode == 'verified': # Simulate a successful verification attempt - attempt = self.create_and_submit_attempt_for_user(self.user) + attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) + attempt.mark_ready() + attempt.submit() attempt.approve() response = self.client.get(reverse('dashboard')) diff --git a/lms/djangoapps/verify_student/management/commands/tests/test_verify_student.py b/lms/djangoapps/verify_student/management/commands/tests/test_verify_student.py index 5cfa3667e6..1d71839e20 100644 --- a/lms/djangoapps/verify_student/management/commands/tests/test_verify_student.py +++ b/lms/djangoapps/verify_student/management/commands/tests/test_verify_student.py @@ -4,14 +4,17 @@ Tests for django admin commands in the verify_student module Lots of imports from verify_student's model tests, since they cover similar ground """ + +import boto from django.conf import settings from django.core.management import call_command +from django.core.management.base import CommandError +from django.test import TestCase from mock import patch from testfixtures import LogCapture from common.test.utils import MockS3BotoMixin from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, SSPVerificationRetryConfig -from lms.djangoapps.verify_student.tests import TestVerificationBase from lms.djangoapps.verify_student.tests.test_models import ( FAKE_SETTINGS, mock_software_secure_post, @@ -25,10 +28,22 @@ LOGGER_NAME = 'retry_photo_verification' # Lots of patching to stub in our own settings, and HTTP posting @patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS) @patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post) -class TestVerifyStudentCommand(MockS3BotoMixin, TestVerificationBase): +class TestVerifyStudentCommand(MockS3BotoMixin, TestCase): """ Tests for django admin commands in the verify_student module """ + def create_and_submit(self, username): + """ + Helper method that lets us create new SoftwareSecurePhotoVerifications + """ + user = UserFactory.create() + attempt = SoftwareSecurePhotoVerification(user=user) + user.profile.name = username + attempt.upload_face_image("Fake Data") + attempt.upload_photo_id_image("More Fake Data") + attempt.mark_ready() + attempt.submit() + return attempt def test_retry_failed_photo_verifications(self): """ @@ -36,58 +51,55 @@ class TestVerifyStudentCommand(MockS3BotoMixin, TestVerificationBase): and re-submit them executes successfully """ # set up some fake data to use... - self.create_upload_and_submit_attempt_for_user() + self.create_and_submit("SuccessfulSally") with patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post_error): - self.create_upload_and_submit_attempt_for_user() - self.create_upload_and_submit_attempt_for_user() - + self.create_and_submit("RetryRoger") + with patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post_error): + self.create_and_submit("RetryRick") # check to make sure we had two successes and two failures; otherwise we've got problems elsewhere - self.assertEqual(SoftwareSecurePhotoVerification.objects.filter(status="submitted").count(), 1) - self.assertEqual(SoftwareSecurePhotoVerification.objects.filter(status='must_retry').count(), 2) - - with self.immediate_on_commit(): - call_command('retry_failed_photo_verifications') + assert len(SoftwareSecurePhotoVerification.objects.filter(status="submitted")) == 1 + assert len(SoftwareSecurePhotoVerification.objects.filter(status='must_retry')) == 2 + call_command('retry_failed_photo_verifications') attempts_to_retry = SoftwareSecurePhotoVerification.objects.filter(status='must_retry') assert not attempts_to_retry - def add_test_config_for_retry_verification(self): - """Setups verification retry configuration.""" - config = SSPVerificationRetryConfig.current() - config.arguments = '--verification-ids 1 2 3' - config.enabled = True - config.save() - def test_args_from_database(self): """Test management command arguments injected from config model.""" # Nothing in the database, should default to disabled + with LogCapture(LOGGER_NAME) as log: call_command('retry_failed_photo_verifications', '--args-from-database') log.check_present( ( LOGGER_NAME, 'WARNING', - 'SSPVerificationRetryConfig is disabled or empty, but --args-from-database was requested.' + u"SSPVerificationRetryConfig is disabled or empty, but --args-from-database was requested." ), ) - self.add_test_config_for_retry_verification() - with patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post_error): - self.create_upload_and_submit_attempt_for_user() - with LogCapture(LOGGER_NAME) as log: - with self.immediate_on_commit(): - call_command('retry_failed_photo_verifications') - log.check_present( - ( - LOGGER_NAME, 'INFO', - 'Attempting to retry {0} failed PhotoVerification submissions'.format(1) - ), - ) + # Add a config + config = SSPVerificationRetryConfig.current() + config.arguments = '--verification-ids 1 2 3' + config.enabled = True + config.save() - with LogCapture(LOGGER_NAME) as log: - with self.immediate_on_commit(): - call_command('retry_failed_photo_verifications', '--args-from-database') + with patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post_error): + self.create_and_submit("RetryRoger") + + with LogCapture(LOGGER_NAME) as log: + call_command('retry_failed_photo_verifications') log.check_present( ( LOGGER_NAME, 'INFO', - 'Fetching retry verification ids from config model' + u"Attempting to retry {0} failed PhotoVerification submissions".format(1) ), ) + + with LogCapture(LOGGER_NAME) as log: + call_command('retry_failed_photo_verifications', '--args-from-database') + + log.check_present( + ( + LOGGER_NAME, 'INFO', + u"Fetching retry verification ids from config model" + ), + ) diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py index 3f27affb7f..cdf9250ae8 100644 --- a/lms/djangoapps/verify_student/models.py +++ b/lms/djangoapps/verify_student/models.py @@ -9,6 +9,7 @@ of a student over a period of time. Right now, the only models are the abstract photo verification process as generic as possible. """ + import base64 import codecs import functools @@ -20,12 +21,13 @@ from datetime import timedelta from email.utils import formatdate import requests +import simplejson import six from config_models.models import ConfigurationModel from django.conf import settings from django.contrib.auth.models import User from django.core.files.base import ContentFile -from django.db import models, transaction +from django.db import models from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible from django.utils.functional import cached_property @@ -44,7 +46,7 @@ from lms.djangoapps.verify_student.ssencrypt import ( from openedx.core.djangoapps.signals.signals import LEARNER_NOW_VERIFIED from openedx.core.storage import get_storage -from .utils import auto_verify_for_testing_enabled, earliest_allowed_verification_date +from .utils import earliest_allowed_verification_date log = logging.getLogger(__name__) @@ -391,7 +393,7 @@ class PhotoVerification(IDVerificationAttempt): # student dashboard. But at this point, we lock the value into the # attempt. self.name = self.user.profile.name - self.status = self.STATUS.ready + self.status = "ready" self.save() @status_before_must_be("must_retry", "submitted", "approved", "denied") @@ -424,7 +426,7 @@ class PhotoVerification(IDVerificationAttempt): logs. This should be a relatively rare occurrence. """ # If someone approves an outdated version of this, the first one wins - if self.status == self.STATUS.approved: + if self.status == "approved": return log.info(u"Verification for user '{user_id}' approved by '{reviewer}'.".format( @@ -434,7 +436,7 @@ class PhotoVerification(IDVerificationAttempt): self.error_code = "" # reset, in case this attempt was denied before self.reviewing_user = user_id self.reviewing_service = service - self.status = self.STATUS.approved + self.status = "approved" self.save() # Emit signal to find and generate eligible certificates LEARNER_NOW_VERIFIED.send_robust( @@ -445,51 +447,6 @@ class PhotoVerification(IDVerificationAttempt): message = u'LEARNER_NOW_VERIFIED signal fired for {user} from PhotoVerification' log.info(message.format(user=self.user.username)) - @status_before_must_be("ready", "must_retry") - def mark_submit(self): - """ - Submit this attempt. - Valid attempt statuses when calling this method: - `ready`, `must_retry` - - Status after method completes: `submitted` - - State Transitions: - - → → → `must_retry` - ↑ ↑ ↓ - `ready` → `submitted` - """ - self.submitted_at = now() - self.status = self.STATUS.submitted - self.save() - - @status_before_must_be("ready", "must_retry", "submitted") - def mark_must_retry(self, error=""): - """ - Set the attempt status to `must_retry`. - Mark that this attempt could not be completed because of a system error. - Status should be moved to `must_retry`. For example, if Software Secure - service is down and we couldn't process the request even after retrying. - - Valid attempt statuses when calling this method: - `ready`, `submitted` - - Status after method completes: `must_retry` - - State Transitions: - - → → → `must_retry` - ↑ ↑ ↓ - `ready` → `submitted` - """ - if self.status == self.STATUS.must_retry: - return - - self.status = self.STATUS.must_retry - self.error_msg = error - self.save() - @status_before_must_be("must_retry", "submitted", "approved", "denied") def deny(self, error_msg, @@ -533,7 +490,7 @@ class PhotoVerification(IDVerificationAttempt): self.error_code = error_code self.reviewing_user = reviewing_user self.reviewing_service = reviewing_service - self.status = self.STATUS.denied + self.status = "denied" self.save() @status_before_must_be("must_retry", "submitted", "approved", "denied") @@ -548,14 +505,14 @@ class PhotoVerification(IDVerificationAttempt): reported to us that they couldn't process our submission because they couldn't decrypt the image we sent. """ - if self.status in [self.STATUS.approved, self.STATUS.denied]: + if self.status in ["approved", "denied"]: return # If we were already approved or denied, just leave it. self.error_msg = error_msg self.error_code = error_code self.reviewing_user = reviewing_user self.reviewing_service = reviewing_service - self.status = self.STATUS.must_retry + self.status = "must_retry" self.save() @classmethod @@ -684,7 +641,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification): # developing and aren't interested in working on student identity # verification functionality. If you do want to work on it, you have to # explicitly enable these in your private settings. - if auto_verify_for_testing_enabled(): + if settings.FEATURES.get('AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'): return aes_key_str = settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["FACE_IMAGE_AES_KEY"] @@ -714,7 +671,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification): # developing and aren't interested in working on student identity # verification functionality. If you do want to work on it, you have to # explicitly enable these in your private settings. - if auto_verify_for_testing_enabled(): + if settings.FEATURES.get('AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'): # fake photo id key is set only for initial verification self.photo_id_key = 'fake-photo-id-key' self.save() @@ -746,28 +703,27 @@ class SoftwareSecurePhotoVerification(PhotoVerification): Keyword Arguments: copy_id_photo_from (SoftwareSecurePhotoVerification): If provided, re-send the ID photo - data from this attempt. This is used for re-verification, in which new face photos + data from this attempt. This is used for reverification, in which new face photos are sent with previously-submitted ID photos. - """ - from .tasks import send_request_to_ss_for_user - if auto_verify_for_testing_enabled(): - self.mark_submit() - fake_response = requests.Response() - fake_response.status_code = 200 - return fake_response - if copy_id_photo_from is not None: - log.info( - ('Software Secure attempt for user: %r and receipt ID: %r used the same photo ID data as the ' - 'receipt with ID %r.'), - self.user.username, - self.receipt_id, - copy_id_photo_from.receipt_id, + """ + try: + response = self.send_request(copy_id_photo_from=copy_id_photo_from) + if response.ok: + self.submitted_at = now() + self.status = "submitted" + self.save() + else: + self.status = "must_retry" + self.error_msg = response.text + self.save() + except Exception: # pylint: disable=broad-except + log.exception( + u'Software Secure submission failed for user %s, setting status to must_retry', + self.user.username ) - transaction.on_commit( - lambda: - send_request_to_ss_for_user.delay(user_verification_id=self.id, copy_id_photo_from=copy_id_photo_from) - ) + self.status = "must_retry" + self.save() def parsed_error_msg(self): """ @@ -810,7 +766,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification): parsed_errors.append(parsed_error) else: log.debug(u'Ignoring photo verification error message: %s', message) - except Exception: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except log.exception(u'Failed to parse error message for SoftwareSecurePhotoVerification %d', self.pk) return parsed_errors @@ -889,7 +845,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification): Keyword Arguments: copy_id_photo_from (SoftwareSecurePhotoVerification): If provided, re-send the ID photo - data from this attempt. This is used for re-verification, in which new face photos + data from this attempt. This is used for reverification, in which new face photos are sent with previously-submitted ID photos. Returns: @@ -955,6 +911,55 @@ class SoftwareSecurePhotoVerification(PhotoVerification): return header_txt + "\n\n" + body_txt + def send_request(self, copy_id_photo_from=None): + """ + Assembles a submission to Software Secure and sends it via HTTPS. + + Keyword Arguments: + copy_id_photo_from (SoftwareSecurePhotoVerification): If provided, re-send the ID photo + data from this attempt. This is used for reverification, in which new face photos + are sent with previously-submitted ID photos. + + Returns: + request.Response + + """ + # If AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING is True, we want to + # skip posting anything to Software Secure. We actually don't even + # create the message because that would require encryption and message + # signing that rely on settings.VERIFY_STUDENT values that aren't set + # in dev. So we just pretend like we successfully posted + if settings.FEATURES.get('AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'): + fake_response = requests.Response() + fake_response.status_code = 200 + return fake_response + + headers, body = self.create_request(copy_id_photo_from=copy_id_photo_from) + + response = requests.post( + settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_URL"], + headers=headers, + data=simplejson.dumps(body, indent=2, sort_keys=True, ensure_ascii=False).encode('utf-8'), + verify=False + ) + + log.info(u"Sent request to Software Secure for receipt ID %s.", self.receipt_id) + if copy_id_photo_from is not None: + log.info( + ( + u"Software Secure attempt with receipt ID %s used the same photo ID " + u"data as the receipt with ID %s" + ), + self.receipt_id, copy_id_photo_from.receipt_id + ) + + log.debug("Headers:\n{}\n\n".format(headers)) + log.debug("Body:\n{}\n\n".format(body)) + log.debug(u"Return code: {}".format(response.status_code)) + log.debug(u"Return message:\n\n{}\n\n".format(response.text)) + + return response + def should_display_status_to_user(self): """Whether or not the status from this attempt should be displayed to the user.""" return True diff --git a/lms/djangoapps/verify_student/services.py b/lms/djangoapps/verify_student/services.py index dd9765d760..3f7b2e28ea 100644 --- a/lms/djangoapps/verify_student/services.py +++ b/lms/djangoapps/verify_student/services.py @@ -148,11 +148,9 @@ class IDVerificationService(object): 'created_at__gte': earliest_allowed_verification_date() } - return ( - SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs).exists() or - SSOVerification.objects.filter(**filter_kwargs).exists() or - ManualVerification.objects.filter(**filter_kwargs).exists() - ) + return (SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs).exists() or + SSOVerification.objects.filter(**filter_kwargs).exists() or + ManualVerification.objects.filter(**filter_kwargs).exists()) @classmethod def user_status(cls, user): diff --git a/lms/djangoapps/verify_student/tasks.py b/lms/djangoapps/verify_student/tasks.py index a4d1aa4f33..d1bd5b8a5f 100644 --- a/lms/djangoapps/verify_student/tasks.py +++ b/lms/djangoapps/verify_student/tasks.py @@ -2,71 +2,21 @@ Django Celery tasks for service status app """ + import logging from smtplib import SMTPException -import requests -import simplejson -from celery import Task, task -from celery.states import FAILURE +from celery import task from django.conf import settings from django.core.mail import send_mail from edxmako.shortcuts import render_to_string -from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers ACE_ROUTING_KEY = getattr(settings, 'ACE_ROUTING_KEY', None) -SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = getattr(settings, 'SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY', None) log = logging.getLogger(__name__) -class BaseSoftwareSecureTask(Task): - """ - Base task class for use with Software Secure request. - - Permits updating information about user attempt in correspondence to submitting - request to software secure. - """ - abstract = True - - def on_success(self, response, task_id, args, kwargs): - """ - Update SoftwareSecurePhotoVerification object corresponding to this - task with info about success. - - Updates user verification attempt to "submitted" if the response was ok otherwise - set it to "must_retry". - """ - user_verification_id = kwargs['user_verification_id'] - user_verification = SoftwareSecurePhotoVerification.objects.get(id=user_verification_id) - if response.ok: - user_verification.mark_submit() - log.info( - 'Sent request to Software Secure for user: %r and receipt ID %r.', - user_verification.user.username, - user_verification.receipt_id, - ) - return user_verification - - user_verification.mark_must_retry(response.text) - - def after_return(self, status, retval, task_id, args, kwargs, einfo): - """ - If max retries have reached and task status is still failing, mark user submission - with "must_retry" so that it can be retried latter. - """ - if self.max_retries == self.request.retries and status == FAILURE: - user_verification_id = kwargs['user_verification_id'] - user_verification = SoftwareSecurePhotoVerification.objects.get(id=user_verification_id) - user_verification.mark_must_retry() - log.error( - 'Software Secure submission failed for user %r, setting status to must_retry', - user_verification.user.username, - exc_info=True - ) - - @task(routing_key=ACE_ROUTING_KEY) def send_verification_status_email(context): """ @@ -90,48 +40,3 @@ def send_verification_status_email(context): ) except SMTPException: log.warning(u"Failure in sending verification status e-mail to %s", dest_addr) - - -@task( - base=BaseSoftwareSecureTask, - bind=True, - default_retry_delay=settings.SOFTWARE_SECURE_REQUEST_RETRY_DELAY, - max_retries=settings.SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS, - routing_key=SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY, -) -def send_request_to_ss_for_user(self, user_verification_id, copy_id_photo_from): - """ - Assembles a submission to Software Secure. - - Keyword Arguments: - user_verification_id (int) SoftwareSecurePhotoVerification model object identifier. - copy_id_photo_from (SoftwareSecurePhotoVerification): If provided, re-send the ID photo - data from this attempt. This is used for re-verification, in which new face photos - are sent with previously-submitted ID photos. - Returns: - request.Response - """ - log.info('=>New Verification Task Received') # todo -- remove before merge. - user_verification = SoftwareSecurePhotoVerification.objects.get(id=user_verification_id) - try: - headers, body = user_verification.create_request(copy_id_photo_from) - response = requests.post( - settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_URL"], - headers=headers, - data=simplejson.dumps(body, indent=2, sort_keys=True, ensure_ascii=False).encode('utf-8'), - verify=False - ) - return response - except Exception as exc: # pylint: disable=bare-except - log.error( - ( - 'Retrying sending request to Software Secure for user: %r, Receipt ID: %r ' - 'attempt#: %s of %s' - ), - user_verification.user.username, - user_verification.receipt_id, - self.request.retries, - settings.SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS, - ) - log.error(str(exc)) - self.retry() diff --git a/lms/djangoapps/verify_student/tests/__init__.py b/lms/djangoapps/verify_student/tests/__init__.py index e889ad708b..e69de29bb2 100644 --- a/lms/djangoapps/verify_student/tests/__init__.py +++ b/lms/djangoapps/verify_student/tests/__init__.py @@ -1,92 +0,0 @@ -from contextlib import contextmanager -from datetime import timedelta -from unittest import mock - -from django.conf import settings -from django.db import DEFAULT_DB_ALIAS -from django.test import TestCase -from django.utils.timezone import now - -from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification -from student.tests.factories import UserFactory - - -class TestVerificationBase(TestCase): - """ - Common tests across all types of Verifications (e.g., SoftwareSecurePhotoVerification, SSOVerification) - """ - - @contextmanager - def immediate_on_commit(self, using=None): - """ - Context manager executing transaction.on_commit() hooks immediately as - if the connection was in auto-commit mode. This is required when - using a subclass of django.test.TestCase as all tests are wrapped in - a transaction that never gets committed. - - TODO: Remove when immediate_on_commit function is actually implemented - Django Ticket #: 30456, Link: https://code.djangoproject.com/ticket/30457#no1 - """ - immediate_using = DEFAULT_DB_ALIAS if using is None else using - - def on_commit(func, using=None): - using = DEFAULT_DB_ALIAS if using is None else using - if using == immediate_using: - func() - - with mock.patch('django.db.transaction.on_commit', side_effect=on_commit) as patch: - yield patch - - def verification_active_at_datetime(self, attempt): - """ - Tests to ensure the Verification is active or inactive at the appropriate datetimes. - """ - # Not active before the created date - before = attempt.created_at - timedelta(seconds=1) - self.assertFalse(attempt.active_at_datetime(before)) - - # Active immediately after created date - after_created = attempt.created_at + timedelta(seconds=1) - self.assertTrue(attempt.active_at_datetime(after_created)) - - # Active immediately before expiration date - expiration = attempt.created_at + timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]) - before_expiration = expiration - timedelta(seconds=1) - self.assertTrue(attempt.active_at_datetime(before_expiration)) - - # Not active after the expiration date - attempt.created_at = attempt.created_at - timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]) - attempt.save() - self.assertFalse(attempt.active_at_datetime(now() + timedelta(days=1))) - - def submit_attempt(self, attempt): - with self.immediate_on_commit(): - attempt.submit() - attempt.refresh_from_db() - return attempt - - def create_and_submit_attempt_for_user(self, user=None): - """ - Create photo verification attempt without uploading photos - for a user. - """ - if not user: - user = UserFactory.create() - attempt = SoftwareSecurePhotoVerification.objects.create(user=user) - attempt.mark_ready() - return self.submit_attempt(attempt) - - def create_upload_and_submit_attempt_for_user(self, user=None): - """ - Helper method to create a generic submission with photos for - a user and send it. - """ - if not user: - user = UserFactory.create() - attempt = SoftwareSecurePhotoVerification(user=user) - user.profile.name = u"Rust\u01B4" - - attempt.upload_face_image("Just pretend this is image data") - attempt.upload_photo_id_image("Hey, we're a photo ID") - attempt.mark_ready() - return self.submit_attempt(attempt) diff --git a/lms/djangoapps/verify_student/tests/test_models.py b/lms/djangoapps/verify_student/tests/test_models.py index c008b01386..fe59f36f23 100644 --- a/lms/djangoapps/verify_student/tests/test_models.py +++ b/lms/djangoapps/verify_student/tests/test_models.py @@ -1,29 +1,30 @@ # -*- coding: utf-8 -*- + import base64 +import simplejson as json from datetime import datetime, timedelta import ddt import mock import requests.exceptions -import simplejson as json from django.conf import settings +from django.test import TestCase from django.utils.timezone import now from freezegun import freeze_time from mock import patch from six.moves import range +from student.tests.factories import UserFactory +from testfixtures import LogCapture +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from common.test.utils import MockS3BotoMixin from lms.djangoapps.verify_student.models import ( - ManualVerification, - PhotoVerification, SoftwareSecurePhotoVerification, SSOVerification, - VerificationException + ManualVerification, + VerificationException, ) -from student.tests.factories import UserFactory -from verify_student.tests import TestVerificationBase -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase FAKE_SETTINGS = { "SOFTWARE_SECURE": { @@ -89,11 +90,39 @@ def mock_software_secure_post_unavailable(url, headers=None, data=None, **kwargs raise requests.exceptions.ConnectionError +class TestVerification(TestCase): + """ + Common tests across all types of Verications (e.g., SoftwareSecurePhotoVerication, SSOVerification) + """ + + def verification_active_at_datetime(self, attempt): + """ + Tests to ensure the Verification is active or inactive at the appropriate datetimes. + """ + # Not active before the created date + before = attempt.created_at - timedelta(seconds=1) + self.assertFalse(attempt.active_at_datetime(before)) + + # Active immediately after created date + after_created = attempt.created_at + timedelta(seconds=1) + self.assertTrue(attempt.active_at_datetime(after_created)) + + # Active immediately before expiration date + expiration = attempt.created_at + timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]) + before_expiration = expiration - timedelta(seconds=1) + self.assertTrue(attempt.active_at_datetime(before_expiration)) + + # Not active after the expiration date + attempt.created_at = attempt.created_at - timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]) + attempt.save() + self.assertFalse(attempt.active_at_datetime(now() + timedelta(days=1))) + + # Lots of patching to stub in our own settings, and HTTP posting @patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS) @patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post) @ddt.ddt -class TestPhotoVerification(TestVerificationBase, MockS3BotoMixin, ModuleStoreTestCase): +class TestPhotoVerification(TestVerification, MockS3BotoMixin, ModuleStoreTestCase): def test_state_transitions(self): """ @@ -109,59 +138,44 @@ class TestPhotoVerification(TestVerificationBase, MockS3BotoMixin, ModuleStoreTe """ user = UserFactory.create() attempt = SoftwareSecurePhotoVerification(user=user) - self.assertEqual(attempt.status, PhotoVerification.STATUS.created) + self.assertEqual(attempt.status, "created") # These should all fail because we're in the wrong starting state. self.assertRaises(VerificationException, attempt.submit) self.assertRaises(VerificationException, attempt.approve) self.assertRaises(VerificationException, attempt.deny) - self.assertRaises(VerificationException, attempt.mark_must_retry) - self.assertRaises(VerificationException, attempt.mark_submit) # Now let's fill in some values so that we can pass the mark_ready() call attempt.mark_ready() - self.assertEqual(attempt.status, PhotoVerification.STATUS.ready) + self.assertEqual(attempt.status, "ready") # ready (can't approve or deny unless it's "submitted") self.assertRaises(VerificationException, attempt.approve) self.assertRaises(VerificationException, attempt.deny) - attempt.mark_must_retry() - attempt.mark_submit() DENY_ERROR_MSG = '[{"photoIdReasons": ["Not provided"]}]' # must_retry - attempt.status = PhotoVerification.STATUS.must_retry + attempt.status = "must_retry" attempt.system_error("System error") - attempt.mark_must_retry() # no-op - attempt.mark_submit() attempt.approve() - - attempt.status = PhotoVerification.STATUS.must_retry + attempt.status = "must_retry" attempt.deny(DENY_ERROR_MSG) # submitted - attempt.status = PhotoVerification.STATUS.submitted + attempt.status = "submitted" attempt.deny(DENY_ERROR_MSG) - - attempt.status = PhotoVerification.STATUS.submitted - attempt.mark_must_retry() - - attempt.status = PhotoVerification.STATUS.submitted + attempt.status = "submitted" attempt.approve() # approved self.assertRaises(VerificationException, attempt.submit) - self.assertRaises(VerificationException, attempt.mark_must_retry) - self.assertRaises(VerificationException, attempt.mark_submit) attempt.approve() # no-op attempt.system_error("System error") # no-op, something processed it without error attempt.deny(DENY_ERROR_MSG) # denied self.assertRaises(VerificationException, attempt.submit) - self.assertRaises(VerificationException, attempt.mark_must_retry) - self.assertRaises(VerificationException, attempt.mark_submit) attempt.deny(DENY_ERROR_MSG) # no-op attempt.system_error("System error") # no-op, something processed it without error attempt.approve() @@ -184,21 +198,39 @@ class TestPhotoVerification(TestVerificationBase, MockS3BotoMixin, ModuleStoreTe self.assertEqual(u"Clyde \u01B4", attempt.name) + def create_and_submit(self): + """Helper method to create a generic submission and send it.""" + user = UserFactory.create() + attempt = SoftwareSecurePhotoVerification(user=user) + user.profile.name = u"Rust\u01B4" + + attempt.upload_face_image("Just pretend this is image data") + attempt.upload_photo_id_image("Hey, we're a photo ID") + attempt.mark_ready() + attempt.submit() + + return attempt + def test_submissions(self): """Test that we set our status correctly after a submission.""" # Basic case, things go well. - attempt = self.create_upload_and_submit_attempt_for_user() - self.assertEqual(attempt.status, PhotoVerification.STATUS.submitted) + attempt = self.create_and_submit() + self.assertEqual(attempt.status, "submitted") # We post, but Software Secure doesn't like what we send for some reason - with patch('lms.djangoapps.verify_student.tasks.requests.post', new=mock_software_secure_post_error): - attempt = self.create_upload_and_submit_attempt_for_user() - self.assertEqual(attempt.status, PhotoVerification.STATUS.must_retry) + with patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post_error): + attempt = self.create_and_submit() + self.assertEqual(attempt.status, "must_retry") # We try to post, but run into an error (in this case a network connection error) - with patch('lms.djangoapps.verify_student.tasks.requests.post', new=mock_software_secure_post_unavailable): - attempt = self.create_upload_and_submit_attempt_for_user() - self.assertEqual(attempt.status, PhotoVerification.STATUS.must_retry) + with patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post_unavailable): + with LogCapture('lms.djangoapps.verify_student.models') as logger: + attempt = self.create_and_submit() + self.assertEqual(attempt.status, "must_retry") + logger.check( + ('lms.djangoapps.verify_student.models', 'ERROR', + u'Software Secure submission failed for user %s, setting status to must_retry' + % attempt.user.username)) @mock.patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) def test_submission_while_testing_flag_is_true(self): @@ -206,14 +238,21 @@ class TestPhotoVerification(TestVerificationBase, MockS3BotoMixin, ModuleStoreTe initial verification when the feature flag 'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING' is enabled. """ - attempt = self.create_upload_and_submit_attempt_for_user() + user = UserFactory.create() + attempt = SoftwareSecurePhotoVerification(user=user) + user.profile.name = "test-user" + + attempt.upload_photo_id_image("Image data") + attempt.mark_ready() + attempt.submit() + self.assertEqual(attempt.photo_id_key, "fake-photo-id-key") # pylint: disable=line-too-long def test_parse_error_msg_success(self): user = UserFactory.create() attempt = SoftwareSecurePhotoVerification(user=user) - attempt.status = PhotoVerification.STATUS.denied + attempt.status = 'denied' attempt.error_msg = '[{"userPhotoReasons": ["Face out of view"]}, {"photoIdReasons": ["Photo hidden/No photo", "ID name not provided"]}]' parsed_error_msg = attempt.parsed_error_msg() self.assertEqual( @@ -249,7 +288,7 @@ class TestPhotoVerification(TestVerificationBase, MockS3BotoMixin, ModuleStoreTe # Make an initial verification with 'photo_id_key' attempt = SoftwareSecurePhotoVerification(user=user, photo_id_key="dummy_photo_id_key") - attempt.status = PhotoVerification.STATUS.approved + attempt.status = 'approved' attempt.save() # Check that method 'get_initial_verification' returns the correct @@ -259,7 +298,7 @@ class TestPhotoVerification(TestVerificationBase, MockS3BotoMixin, ModuleStoreTe # Now create a second verification without 'photo_id_key' attempt = SoftwareSecurePhotoVerification(user=user) - attempt.status = PhotoVerification.STATUS.submitted + attempt.status = 'submitted' attempt.save() # Test method 'get_initial_verification' still returns the correct @@ -293,7 +332,7 @@ class TestPhotoVerification(TestVerificationBase, MockS3BotoMixin, ModuleStoreTe # Populate Record attempt.mark_ready() - attempt.status = PhotoVerification.STATUS.submitted + attempt.status = "submitted" attempt.photo_id_image_url = "https://example.com/test/image/img.jpg" attempt.face_image_url = "https://example.com/test/face/img.jpg" attempt.photo_id_key = 'there_was_an_attempt' @@ -340,7 +379,7 @@ class TestPhotoVerification(TestVerificationBase, MockS3BotoMixin, ModuleStoreTe for _ in range(2): # Make an approved verification attempt = SoftwareSecurePhotoVerification(user=user) - attempt.status = PhotoVerification.STATUS.approved + attempt.status = 'approved' attempt.expiry_date = datetime.now() attempt.save() @@ -361,7 +400,7 @@ class TestPhotoVerification(TestVerificationBase, MockS3BotoMixin, ModuleStoreTe for _ in range(2): # Make an approved verification attempt = SoftwareSecurePhotoVerification(user=user) - attempt.status = PhotoVerification.STATUS.approved + attempt.status = 'approved' attempt.save() # Test method 'get_recent_verification' returns None @@ -389,7 +428,7 @@ class TestPhotoVerification(TestVerificationBase, MockS3BotoMixin, ModuleStoreTe user = UserFactory.create() verification = SoftwareSecurePhotoVerification(user=user) verification.expiry_date = now() - timedelta(days=FAKE_SETTINGS['DAYS_GOOD_FOR']) - verification.status = PhotoVerification.STATUS.approved + verification.status = 'approved' verification.save() self.assertIsNone(verification.expiry_email_date) @@ -400,7 +439,7 @@ class TestPhotoVerification(TestVerificationBase, MockS3BotoMixin, ModuleStoreTe self.assertIsNotNone(result.expiry_email_date) -class SSOVerificationTest(TestVerificationBase): +class SSOVerificationTest(TestVerification): """ Tests for the SSOVerification model """ @@ -411,7 +450,7 @@ class SSOVerificationTest(TestVerificationBase): self.verification_active_at_datetime(attempt) -class ManualVerificationTest(TestVerificationBase): +class ManualVerificationTest(TestVerification): """ Tests for the ManualVerification model """ diff --git a/lms/djangoapps/verify_student/tests/test_tasks.py b/lms/djangoapps/verify_student/tests/test_tasks.py deleted file mode 100644 index dd04a4d23b..0000000000 --- a/lms/djangoapps/verify_student/tests/test_tasks.py +++ /dev/null @@ -1,38 +0,0 @@ -# Lots of patching to stub in our own settings, and HTTP posting -import ddt -import mock -from django.conf import settings -from mock import patch - -from common.test.utils import MockS3BotoMixin -from verify_student.tests import TestVerificationBase -from verify_student.tests.test_models import FAKE_SETTINGS, mock_software_secure_post_unavailable -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase - -LOGGER_NAME = 'lms.djangoapps.verify_student.tasks' - - -@patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS) -@ddt.ddt -class TestPhotoVerificationTasks(TestVerificationBase, MockS3BotoMixin, ModuleStoreTestCase): - - @mock.patch('lms.djangoapps.verify_student.tasks.log') - def test_logs_for_retry_until_failure(self, mock_log): - retry_max_attempts = settings.SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS - with mock.patch('lms.djangoapps.verify_student.tasks.requests.post', new=mock_software_secure_post_unavailable): - attempt = self.create_and_submit_attempt_for_user() - username = attempt.user.username - mock_log.error.assert_called_with( - 'Software Secure submission failed for user %r, setting status to must_retry', - username, - exc_info=True - ) - for current_attempt in range(retry_max_attempts): - mock_log.error.assert_any_call( - ('Retrying sending request to Software Secure for user: %r, Receipt ID: %r ' - 'attempt#: %s of %s'), - username, - attempt.receipt_id, - current_attempt, - settings.SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS, - ) diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index e12acdcdcc..e6ed3a6381 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -3,6 +3,7 @@ Tests of verify_student views. """ + from datetime import timedelta from uuid import uuid4 @@ -44,7 +45,6 @@ from shoppingcart.models import CertificateItem, Order from student.models import CourseEnrollment from student.tests.factories import CourseEnrollmentFactory, UserFactory from util.testing import UrlResetMixin -from verify_student.tests import TestVerificationBase from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -54,7 +54,6 @@ from xmodule.modulestore.tests.factories import CourseFactory def mock_render_to_response(*args, **kwargs): return render_to_response(*args, **kwargs) - render_mock = Mock(side_effect=mock_render_to_response) PAYMENT_DATA_KEYS = {'payment_processor_name', 'payment_page_url', 'payment_form_data'} @@ -65,7 +64,6 @@ class StartView(TestCase): This view is for the first time student is attempting a Photo Verification. """ - def start_url(self, course_id=""): return "/verify_student/{0}".format(six.moves.urllib.parse.quote(course_id)) @@ -82,7 +80,7 @@ class StartView(TestCase): @ddt.ddt -class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin, TestVerificationBase): +class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): """ Tests for the payment and verification flow views. """ @@ -899,7 +897,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin, Tes if status in ["submitted", "approved", "expired", "denied", "error"]: attempt.mark_ready() - attempt = self.submit_attempt(attempt) + attempt.submit() if status in ["approved", "expired"]: attempt.approve() @@ -1114,7 +1112,6 @@ class CheckoutTestMixin(object): compatibility, the effect of using this endpoint is to choose a specific product (i.e. course mode) and trigger immediate checkout. """ - def setUp(self): """ Create a user and course. """ super(CheckoutTestMixin, self).setUp() @@ -1126,12 +1123,12 @@ class CheckoutTestMixin(object): self.client.login(username="test", password="test") def _assert_checked_out( - self, - post_params, - patched_create_order, - expected_course_key, - expected_mode_slug, - expected_status_code=200 + self, + post_params, + patched_create_order, + expected_course_key, + expected_mode_slug, + expected_status_code=200 ): """ DRY helper. @@ -1416,7 +1413,7 @@ class TestCreateOrderView(ModuleStoreTestCase): @ddt.ddt @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) -class TestSubmitPhotosForVerification(MockS3BotoMixin, TestVerificationBase): +class TestSubmitPhotosForVerification(MockS3BotoMixin, TestCase): """ Tests for submitting photos for verification. """ @@ -1574,7 +1571,6 @@ class TestSubmitPhotosForVerification(MockS3BotoMixin, TestVerificationBase): # Now the request should succeed self._submit_photos(face_image=self.IMAGE_DATA) - # def _submit_photos(self, face_image=None, photo_id_image=None, full_name=None, expected_status_code=200): """Submit photos for verification. @@ -1600,8 +1596,7 @@ class TestSubmitPhotosForVerification(MockS3BotoMixin, TestVerificationBase): if full_name is not None: params['full_name'] = full_name - with self.immediate_on_commit(): - response = self.client.post(url, params) + response = self.client.post(url, params) self.assertEqual(response.status_code, expected_status_code) return response @@ -1642,11 +1637,10 @@ class TestSubmitPhotosForVerification(MockS3BotoMixin, TestVerificationBase): return json.loads(last_request.body) -class TestPhotoVerificationResultsCallback(ModuleStoreTestCase, TestVerificationBase): +class TestPhotoVerificationResultsCallback(ModuleStoreTestCase): """ Tests for the results_callback view. """ - def setUp(self): super(TestPhotoVerificationResultsCallback, self).setUp() @@ -1756,7 +1750,9 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase, TestVerification expiry_date = now() + timedelta( days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"] ) - verification = self.create_and_submit_attempt_for_user(self.user) + verification = SoftwareSecurePhotoVerification.objects.create(user=self.user) + verification.mark_ready() + verification.submit() verification.approve() verification.expiry_date = now() verification.expiry_email_date = now() @@ -1900,11 +1896,11 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase, TestVerification self.assertContains(response, 'Result Unknown not understood', status_code=400) -class TestReverifyView(TestVerificationBase): +class TestReverifyView(TestCase): """ - Tests for the re-verification view. + Tests for the reverification view. - Re-verification occurs when a verification attempt is denied or expired, + Reverification occurs when a verification attempt is denied or expired, and the student is given the option to resubmit. """ @@ -1919,26 +1915,30 @@ class TestReverifyView(TestVerificationBase): def test_reverify_view_can_do_initial_verification(self): """ - Test that a User can use re-verify link for initial verification. + Test that a User can use reverify link for initial verification. """ self._assert_can_reverify() def test_reverify_view_can_reverify_denied(self): - # User has a denied attempt, so can re-verify - attempt = self.create_and_submit_attempt_for_user(self.user) + # User has a denied attempt, so can reverify + attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) + attempt.mark_ready() + attempt.submit() attempt.deny("error") self._assert_can_reverify() def test_reverify_view_can_reverify_expired(self): # User has a verification attempt, but it's expired - attempt = self.create_and_submit_attempt_for_user(self.user) + attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) + attempt.mark_ready() + attempt.submit() attempt.approve() days_good_for = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"] attempt.created_at = now() - timedelta(days=(days_good_for + 1)) attempt.save() - # Allow the student to re-verify + # Allow the student to reverify self._assert_can_reverify() def test_reverify_view_can_reverify_pending(self): @@ -1951,17 +1951,21 @@ class TestReverifyView(TestVerificationBase): """ # User has submitted a verification attempt, but Software Secure has not yet responded - attempt = self.create_and_submit_attempt_for_user(self.user) + attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) + attempt.mark_ready() + attempt.submit() # Can re-verify because an attempt has already been submitted. self._assert_can_reverify() def test_reverify_view_cannot_reverify_approved(self): # Submitted attempt has been approved - attempt = self.create_and_submit_attempt_for_user(self.user) + attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) + attempt.mark_ready() + attempt.submit() attempt.approve() - # Cannot re-verify because the user is already verified. + # Cannot reverify because the user is already verified. self._assert_cannot_reverify() @override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10}) @@ -1972,7 +1976,10 @@ class TestReverifyView(TestVerificationBase): and learner can submit photos if verification is set to expire in EXPIRING_SOON_WINDOW(i.e here it is 10 days) or less days. """ - attempt = self.create_and_submit_attempt_for_user(self.user) + + attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) + attempt.mark_ready() + attempt.submit() attempt.approve() # Can re-verify because verification is set to expired soon. @@ -1980,21 +1987,21 @@ class TestReverifyView(TestVerificationBase): def _get_reverify_page(self): """ - Retrieve the re-verification page and return the response. + Retrieve the reverification page and return the response. """ url = reverse("verify_student_reverify") return self.client.get(url) def _assert_can_reverify(self): """ - Check that the re-verification flow is rendered. + Check that the reverification flow is rendered. """ response = self._get_reverify_page() self.assertContains(response, "reverify-container") def _assert_cannot_reverify(self): """ - Check that the user is blocked from re-verifying. + Check that the user is blocked from reverifying. """ response = self._get_reverify_page() self.assertContains(response, "reverify-blocked") diff --git a/lms/djangoapps/verify_student/utils.py b/lms/djangoapps/verify_student/utils.py index 111ae1bdb6..cbe4441c03 100644 --- a/lms/djangoapps/verify_student/utils.py +++ b/lms/djangoapps/verify_student/utils.py @@ -101,18 +101,3 @@ def most_recent_verification(photo_id_verifications, sso_id_verifications, manua } return max(verifications_map, key=lambda k: verifications_map[k]) if verifications_map else None - - -def auto_verify_for_testing_enabled(override=None): - """ - If AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING is True, we want to skip posting - anything to Software Secure. - - Bypass posting anything to Software Secure if auto verify feature for testing is enabled. - We actually don't even create the message because that would require encryption and message - signing that rely on settings.VERIFY_STUDENT values that aren't set in dev. So we just - pretend like we successfully posted. - """ - if override is not None: - return override - return settings.FEATURES.get('AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING') diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 8c950f10ef..6d8d438771 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -12,6 +12,7 @@ import six import six.moves.urllib.error # pylint: disable=import-error import six.moves.urllib.parse # pylint: disable=import-error import six.moves.urllib.request # pylint: disable=import-error +from course_modes.models import CourseMode from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib.staticfiles.storage import staticfiles_storage @@ -28,12 +29,18 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from django.views.generic.base import View from edx_rest_api_client.exceptions import SlumberBaseException +from edxmako.shortcuts import render_to_response, render_to_string from ipware.ip import get_ip from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey +from shoppingcart.models import CertificateItem, Order +from shoppingcart.processors import get_purchase_endpoint, get_signed_purchase_params +from student.models import CourseEnrollment +from track import segment +from util.db import outer_atomic +from util.json_request import JsonResponse +from xmodule.modulestore.django import modulestore -from course_modes.models import CourseMode -from edxmako.shortcuts import render_to_response, render_to_string from lms.djangoapps.commerce.utils import EcommerceService, is_account_activation_requirement_disabled from lms.djangoapps.verify_student.image import InvalidImageData, decode_image_data from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline @@ -48,13 +55,6 @@ from openedx.core.djangoapps.user_api.accounts import NAME_MIN_LENGTH from openedx.core.djangoapps.user_api.accounts.api import update_account_settings from openedx.core.djangoapps.user_api.errors import AccountValidationError, UserNotFound from openedx.core.lib.log_utils import audit_log -from shoppingcart.models import CertificateItem, Order -from shoppingcart.processors import get_purchase_endpoint, get_signed_purchase_params -from student.models import CourseEnrollment -from track import segment -from util.db import outer_atomic -from util.json_request import JsonResponse -from xmodule.modulestore.django import modulestore log = logging.getLogger(__name__) @@ -243,7 +243,7 @@ class PayAndVerifyView(View): # Verify that the course exists if course is None: - log.warning(u"Could not find course with ID %s.", course_id) + log.warn(u"Could not find course with ID %s.", course_id) raise Http404 # Check whether the user has access to this course @@ -299,7 +299,7 @@ class PayAndVerifyView(View): else: # Otherwise, there has never been a verified/paid mode, # so return a page not found response. - log.warning( + log.warn( u"No paid/verified course mode found for course '%s' for verification/payment flow request", course_id ) @@ -822,12 +822,12 @@ def create_order(request): paid_modes = CourseMode.paid_modes_for_course(course_id) if paid_modes: if len(paid_modes) > 1: - log.warning(u"Multiple paid course modes found for course '%s' for create order request", course_id) + log.warn(u"Multiple paid course modes found for course '%s' for create order request", course_id) current_mode = paid_modes[0] # Make sure this course has a paid mode if not current_mode: - log.warning(u"Create order requested for course '%s' without a paid mode.", course_id) + log.warn(u"Create order requested for course '%s' without a paid mode.", course_id) return HttpResponseBadRequest(_("This course doesn't support paid certificates")) if CourseMode.is_professional_mode(current_mode): @@ -903,7 +903,7 @@ class SubmitPhotosView(View): # Retrieve the image data # Validation ensures that we'll have a face image, but we may not have - # a photo ID image if this is a re-verification. + # a photo ID image if this is a reverification. face_image, photo_id_image, response = self._decode_image_data( params["face_image"], params.get("photo_id_image") ) diff --git a/lms/envs/common.py b/lms/envs/common.py index a9ae780f35..3eced93993 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -511,13 +511,6 @@ XQUEUE_INTERFACE = { # Used with Email sending RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5 RETRY_ACTIVATION_EMAIL_TIMEOUT = 0.5 - -# Software Secure request retry settings -# Time in seconds before a retry of the task should be 60 mints. -SOFTWARE_SECURE_REQUEST_RETRY_DELAY = 60 * 60 -# Maximum of 6 retries before giving up. -SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS = 6 - PASSWORD_RESET_EMAIL_RATE_LIMIT = { 'no_of_emails': 1, 'per_seconds': 60 @@ -2161,6 +2154,7 @@ CELERY_BROKER_TRANSPORT = 'amqp' CELERY_BROKER_HOSTNAME = 'localhost' CELERY_BROKER_USER = 'celery' CELERY_BROKER_PASSWORD = 'celery' +CELERY_TIMEZONE = 'UTC' ################################ Block Structures ################################### @@ -2843,8 +2837,6 @@ POLICY_CHANGE_GRADES_ROUTING_KEY = 'edx.lms.core.default' RECALCULATE_GRADES_ROUTING_KEY = 'edx.lms.core.default' -SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = 'edx.lms.core.default' - GRADES_DOWNLOAD = { 'STORAGE_CLASS': 'django.core.files.storage.FileSystemStorage', 'STORAGE_KWARGS': { diff --git a/lms/envs/production.py b/lms/envs/production.py index 92faf57cea..2aa0bd07d0 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -50,7 +50,7 @@ CONFIG_FILE = get_env_setting('LMS_CFG') with codecs.open(CONFIG_FILE, encoding='utf-8') as f: __config__ = yaml.safe_load(f) - # ENV_TOKENS and AUTH_TOKENS are included for reverse compatibility. + # ENV_TOKENS and AUTH_TOKENS are included for reverse compatability. # Removing them may break plugins that rely on them. ENV_TOKENS = __config__ AUTH_TOKENS = __config__ @@ -945,10 +945,6 @@ CREDENTIALS_GENERATION_ROUTING_KEY = ENV_TOKENS.get('CREDENTIALS_GENERATION_ROUT # Queue to use for award program certificates PROGRAM_CERTIFICATES_ROUTING_KEY = ENV_TOKENS.get('PROGRAM_CERTIFICATES_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE) -SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = ENV_TOKENS.get( - 'SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY', - HIGH_PRIORITY_QUEUE -) API_ACCESS_MANAGER_EMAIL = ENV_TOKENS.get('API_ACCESS_MANAGER_EMAIL') API_ACCESS_FROM_EMAIL = ENV_TOKENS.get('API_ACCESS_FROM_EMAIL') diff --git a/openedx/tests/settings.py b/openedx/tests/settings.py index de2260885b..a80a657265 100644 --- a/openedx/tests/settings.py +++ b/openedx/tests/settings.py @@ -123,9 +123,3 @@ RETIREMENT_SERVICE_WORKER_USERNAME = 'RETIREMENT_SERVICE_USER' RETIRED_USERNAME_PREFIX = 'retired__user_' PROCTORING_SETTINGS = {} - -# Software Secure request retry settings -# Time in seconds before a retry of the task should be 60 mints. -SOFTWARE_SECURE_REQUEST_RETRY_DELAY = 60 * 60 -# Maximum of 6 retries before giving up. -SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS = 6