Skip to content
Snippets Groups Projects
Unverified Commit e2069f97 authored by Simon Chen's avatar Simon Chen Committed by GitHub
Browse files

MST-348 Add the API to provide detailed ID Verification information (#24846)

parent a96b30d2
Branches
Tags release-2020-05-26-15.05
No related merge requests found
......@@ -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)
""" 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
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment