MST-348 Add the API to provide detailed ID Verification information (#24846)
This commit is contained in:
@@ -7,7 +7,12 @@ from django.contrib.auth.models import User
|
||||
from django.utils.timezone import now
|
||||
from rest_framework import serializers
|
||||
|
||||
from lms.djangoapps.verify_student.models import ManualVerification, SoftwareSecurePhotoVerification, SSOVerification
|
||||
from lms.djangoapps.verify_student.models import (
|
||||
IDVerificationAttempt,
|
||||
ManualVerification,
|
||||
SoftwareSecurePhotoVerification,
|
||||
SSOVerification
|
||||
)
|
||||
|
||||
from .models import UserPreference
|
||||
|
||||
@@ -130,3 +135,27 @@ class ManualVerificationSerializer(IDVerificationSerializer):
|
||||
class Meta(object):
|
||||
fields = ('status', 'expiration_datetime', 'is_verified')
|
||||
model = ManualVerification
|
||||
|
||||
|
||||
class IDVerificationDetailsSerializer(serializers.Serializer):
|
||||
type = serializers.SerializerMethodField()
|
||||
status = serializers.CharField()
|
||||
expiration_datetime = serializers.DateTimeField()
|
||||
message = serializers.SerializerMethodField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
|
||||
def get_type(self, obj):
|
||||
if isinstance(obj, SoftwareSecurePhotoVerification):
|
||||
return 'Software Secure'
|
||||
elif isinstance(obj, ManualVerification):
|
||||
return 'Manual'
|
||||
else:
|
||||
return 'SSO'
|
||||
|
||||
def get_message(self, obj):
|
||||
if isinstance(obj, SoftwareSecurePhotoVerification):
|
||||
return obj.error_msg
|
||||
elif isinstance(obj, ManualVerification):
|
||||
return obj.reason
|
||||
else:
|
||||
return ''
|
||||
|
||||
@@ -18,7 +18,7 @@ from .accounts.views import (
|
||||
UsernameReplacementView
|
||||
)
|
||||
from .preferences.views import PreferencesDetailView, PreferencesView
|
||||
from .verification_api.views import IDVerificationStatusView
|
||||
from .verification_api.views import IDVerificationStatusView, IDVerificationStatusDetailsView
|
||||
|
||||
ME = AccountViewSet.as_view({
|
||||
'get': 'get',
|
||||
@@ -106,6 +106,11 @@ urlpatterns = [
|
||||
IDVerificationStatusView.as_view(),
|
||||
name='verification_status'
|
||||
),
|
||||
url(
|
||||
r'^v1/accounts/{}/verifications/$'.format(settings.USERNAME_PATTERN),
|
||||
IDVerificationStatusDetailsView.as_view(),
|
||||
name='verification_details'
|
||||
),
|
||||
url(
|
||||
r'^v1/accounts/{}/retirement_status/$'.format(settings.USERNAME_PATTERN),
|
||||
RETIREMENT_RETRIEVE,
|
||||
|
||||
@@ -10,16 +10,17 @@ from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
from lms.djangoapps.verify_student.models import ManualVerification, SoftwareSecurePhotoVerification, SSOVerification
|
||||
from lms.djangoapps.verify_student.tests.factories import SSOVerificationFactory
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
FROZEN_TIME = '2015-01-01'
|
||||
VERIFY_STUDENT = {'DAYS_GOOD_FOR': 365}
|
||||
|
||||
|
||||
@override_settings(VERIFY_STUDENT=VERIFY_STUDENT)
|
||||
class PhotoVerificationStatusViewTests(TestCase):
|
||||
""" Tests for the PhotoVerificationStatusView endpoint. """
|
||||
class VerificationStatusViewTestsMixin:
|
||||
""" Base class for the tests on verification status views """
|
||||
VIEW_NAME = None
|
||||
CREATED_AT = datetime.datetime.strptime(FROZEN_TIME, '%Y-%m-%d')
|
||||
PASSWORD = 'test'
|
||||
|
||||
@@ -27,12 +28,11 @@ class PhotoVerificationStatusViewTests(TestCase):
|
||||
freezer = freezegun.freeze_time(FROZEN_TIME)
|
||||
freezer.start()
|
||||
self.addCleanup(freezer.stop)
|
||||
|
||||
super(PhotoVerificationStatusViewTests, self).setUp()
|
||||
super().setUp()
|
||||
self.user = UserFactory(password=self.PASSWORD)
|
||||
self.staff = UserFactory(is_staff=True, password=self.PASSWORD)
|
||||
self.verification = SoftwareSecurePhotoVerification.objects.create(user=self.user, status='submitted')
|
||||
self.path = reverse('verification_status', kwargs={'username': self.user.username})
|
||||
self.photo_verification = SoftwareSecurePhotoVerification.objects.create(user=self.user, status='submitted')
|
||||
self.path = reverse(self.VIEW_NAME, kwargs={'username': self.user.username})
|
||||
self.client.login(username=self.staff.username, password=self.PASSWORD)
|
||||
|
||||
def assert_path_not_found(self, path):
|
||||
@@ -40,36 +40,30 @@ class PhotoVerificationStatusViewTests(TestCase):
|
||||
response = self.client.get(path)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def get_expected_response(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def assert_verification_returned(self, verified=False):
|
||||
""" Assert the path returns HTTP 200 and returns appropriately-serialized data. """
|
||||
response = self.client.get(self.path)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
expected_expires = self.CREATED_AT + datetime.timedelta(settings.VERIFY_STUDENT['DAYS_GOOD_FOR'])
|
||||
|
||||
expected = {
|
||||
'status': self.verification.status,
|
||||
'expiration_datetime': '{}Z'.format(expected_expires.isoformat()),
|
||||
'is_verified': verified
|
||||
}
|
||||
expected = self.get_expected_response(verified=verified, expected_expires=expected_expires)
|
||||
self.assertEqual(json.loads(response.content.decode('utf-8')), expected)
|
||||
|
||||
def test_non_existent_user(self):
|
||||
""" The endpoint should return HTTP 404 if the user does not exist. """
|
||||
path = reverse('verification_status', kwargs={'username': 'abc123'})
|
||||
self.assert_path_not_found(path)
|
||||
|
||||
def test_no_verifications(self):
|
||||
""" The endpoint should return HTTP 404 if the user has no verifications. """
|
||||
user = UserFactory()
|
||||
path = reverse('verification_status', kwargs={'username': user.username})
|
||||
self.assert_path_not_found(path)
|
||||
|
||||
def test_authentication_required(self):
|
||||
""" The endpoint should return HTTP 403 if the user is not authenticated. """
|
||||
self.client.logout()
|
||||
response = self.client.get(self.path)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_no_verifications(self):
|
||||
""" The endpoint should return HTTP 404 if the user has no verifications. """
|
||||
user = UserFactory()
|
||||
path = reverse(self.VIEW_NAME, kwargs={'username': user.username})
|
||||
self.assert_path_not_found(path)
|
||||
|
||||
def test_staff_user(self):
|
||||
""" The endpoint should be accessible to staff users. """
|
||||
self.client.logout()
|
||||
@@ -82,17 +76,127 @@ class PhotoVerificationStatusViewTests(TestCase):
|
||||
self.client.login(username=self.user.username, password=self.PASSWORD)
|
||||
self.assert_verification_returned()
|
||||
|
||||
def test_non_owner_or_staff_user(self):
|
||||
def test_non_owner_nor_staff_user(self):
|
||||
""" The endpoint should NOT be accessible if the request is not made by the submitter or staff user. """
|
||||
user = UserFactory()
|
||||
self.client.login(username=user.username, password=self.PASSWORD)
|
||||
response = self.client.get(self.path)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_non_existent_user(self):
|
||||
""" The endpoint should return HTTP 404 if the user does not exist. """
|
||||
path = reverse(self.VIEW_NAME, kwargs={'username': 'abc123'})
|
||||
self.assert_path_not_found(path)
|
||||
|
||||
|
||||
@override_settings(VERIFY_STUDENT=VERIFY_STUDENT)
|
||||
class PhotoVerificationStatusViewTests(VerificationStatusViewTestsMixin, TestCase):
|
||||
""" Tests for the PhotoVerificationStatusView endpoint. """
|
||||
VIEW_NAME = 'verification_status'
|
||||
|
||||
def get_expected_response(self, *args, **kwargs):
|
||||
return {
|
||||
'status': self.photo_verification.status,
|
||||
'expiration_datetime': '{}Z'.format(kwargs.get('expected_expires').isoformat()),
|
||||
'is_verified': kwargs.get('verified')
|
||||
}
|
||||
|
||||
def test_approved_verification(self):
|
||||
""" The endpoint should return that the user is verified if the user's verification is accepted. """
|
||||
self.verification.status = 'approved'
|
||||
self.verification.save()
|
||||
self.photo_verification.status = 'approved'
|
||||
self.photo_verification.save()
|
||||
self.client.logout()
|
||||
self.client.login(username=self.user.username, password=self.PASSWORD)
|
||||
self.assert_verification_returned(verified=True)
|
||||
|
||||
|
||||
@override_settings(VERIFY_STUDENT=VERIFY_STUDENT)
|
||||
class VerificationsDetailsViewTests(VerificationStatusViewTestsMixin, TestCase):
|
||||
""" Tests for the IDVerificationDetails endpoint. """
|
||||
VIEW_NAME = 'verification_details'
|
||||
|
||||
def get_expected_response(self, *args, **kwargs):
|
||||
return [{
|
||||
'type': 'Software Secure',
|
||||
'status': self.photo_verification.status,
|
||||
'expiration_datetime': '{}Z'.format(kwargs.get('expected_expires').isoformat()),
|
||||
'message': '',
|
||||
'updated_at': '{}Z'.format(self.CREATED_AT.isoformat())
|
||||
}]
|
||||
|
||||
def test_multiple_verification_types(self):
|
||||
self.manual_verification = ManualVerification.objects.create(
|
||||
user=self.user,
|
||||
status='approved',
|
||||
reason='testing'
|
||||
)
|
||||
self.sso_verification = SSOVerificationFactory(user=self.user, status='approved')
|
||||
self.photo_verification.error_msg = 'tested_error'
|
||||
self.photo_verification.error_code = 'error_code'
|
||||
self.photo_verification.status = 'denied'
|
||||
self.photo_verification.save()
|
||||
response = self.client.get(self.path)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
expected_expires = self.CREATED_AT + datetime.timedelta(settings.VERIFY_STUDENT['DAYS_GOOD_FOR'])
|
||||
|
||||
expected = [
|
||||
{
|
||||
'type': 'Software Secure',
|
||||
'status': self.photo_verification.status,
|
||||
'expiration_datetime': '{}Z'.format(expected_expires.isoformat()),
|
||||
'message': self.photo_verification.error_msg,
|
||||
'updated_at': '{}Z'.format(self.CREATED_AT.isoformat()),
|
||||
},
|
||||
{
|
||||
'type': 'SSO',
|
||||
'status': self.sso_verification.status,
|
||||
'expiration_datetime': '{}Z'.format(expected_expires.isoformat()),
|
||||
'message': '',
|
||||
'updated_at': '{}Z'.format(self.CREATED_AT.isoformat()),
|
||||
},
|
||||
{
|
||||
'type': 'Manual',
|
||||
'status': self.manual_verification.status,
|
||||
'expiration_datetime': '{}Z'.format(expected_expires.isoformat()),
|
||||
'message': self.manual_verification.reason,
|
||||
'updated_at': '{}Z'.format(self.CREATED_AT.isoformat()),
|
||||
},
|
||||
]
|
||||
self.assertEqual(json.loads(response.content.decode('utf-8')), expected)
|
||||
|
||||
def test_multiple_verification_instances(self):
|
||||
self.sso_verification = SSOVerificationFactory(user=self.user, status='approved')
|
||||
second_ss_photo_verification = SoftwareSecurePhotoVerification.objects.create(
|
||||
user=self.user,
|
||||
status='denied',
|
||||
error_msg='test error message for denial',
|
||||
error_code='plain_code'
|
||||
)
|
||||
response = self.client.get(self.path)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
expected_expires = self.CREATED_AT + datetime.timedelta(settings.VERIFY_STUDENT['DAYS_GOOD_FOR'])
|
||||
|
||||
expected = [
|
||||
{
|
||||
'type': 'Software Secure',
|
||||
'status': self.photo_verification.status,
|
||||
'expiration_datetime': '{}Z'.format(expected_expires.isoformat()),
|
||||
'message': self.photo_verification.error_msg,
|
||||
'updated_at': '{}Z'.format(self.CREATED_AT.isoformat()),
|
||||
},
|
||||
{
|
||||
'type': 'Software Secure',
|
||||
'status': second_ss_photo_verification.status,
|
||||
'expiration_datetime': '{}Z'.format(expected_expires.isoformat()),
|
||||
'message': second_ss_photo_verification.error_msg,
|
||||
'updated_at': '{}Z'.format(self.CREATED_AT.isoformat()),
|
||||
},
|
||||
{
|
||||
'type': 'SSO',
|
||||
'status': self.sso_verification.status,
|
||||
'expiration_datetime': '{}Z'.format(expected_expires.isoformat()),
|
||||
'message': '',
|
||||
'updated_at': '{}Z'.format(self.CREATED_AT.isoformat()),
|
||||
},
|
||||
]
|
||||
self.assertEqual(json.loads(response.content.decode('utf-8')), expected)
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
""" Verification API v1 views. """
|
||||
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.http import Http404
|
||||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
from openedx.core.lib.api.authentication import BearerAuthentication
|
||||
from rest_framework.generics import ListAPIView, RetrieveAPIView
|
||||
|
||||
from lms.djangoapps.verify_student.models import ManualVerification, SoftwareSecurePhotoVerification, SSOVerification
|
||||
from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from lms.djangoapps.verify_student.utils import most_recent_verification
|
||||
from openedx.core.djangoapps.user_api.serializers import (
|
||||
IDVerificationDetailsSerializer,
|
||||
ManualVerificationSerializer,
|
||||
SoftwareSecurePhotoVerificationSerializer,
|
||||
SSOVerificationSerializer
|
||||
)
|
||||
from openedx.core.lib.api.authentication import BearerAuthentication
|
||||
from openedx.core.lib.api.permissions import IsStaffOrOwner
|
||||
|
||||
|
||||
@@ -53,3 +55,27 @@ class IDVerificationStatusView(RetrieveAPIView):
|
||||
return verification
|
||||
|
||||
raise Http404
|
||||
|
||||
|
||||
class IDVerificationStatusDetailsView(ListAPIView):
|
||||
""" IDVerificationStatusDeetails endpoint to retrieve more details about ID Verification status """
|
||||
authentication_classes = (JwtAuthentication, BearerAuthentication, SessionAuthentication,)
|
||||
permission_classes = (IsStaffOrOwner,)
|
||||
pagination_class = None # No need for pagination for this yet
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
return IDVerificationDetailsSerializer(*args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
username = self.kwargs['username']
|
||||
User = get_user_model()
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
verifications = IDVerificationService.verifications_for_user(user)
|
||||
if not verifications:
|
||||
raise Http404
|
||||
|
||||
return sorted(verifications, key=lambda x: x.updated_at, reverse=True)
|
||||
except User.DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
Reference in New Issue
Block a user