Skip to content

Commit

Permalink
Add API endpoint for phone verification(2) (#1748)
Browse files Browse the repository at this point in the history
* Add API endpoint for phone verification

* Changes addressed to PR
  • Loading branch information
valaparthvi committed Aug 31, 2017
1 parent 060ca58 commit bddd68e
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 0 deletions.
40 changes: 40 additions & 0 deletions cadasta/accounts/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,43 @@ def validate_new_password(self, password):
_("Passwords cannot contain your phone."))

return password


class PhoneVerificationSerializer(serializers.Serializer,
SanitizeFieldSerializer):
phone = serializers.RegexField(
regex=r'^\+(?:[0-9]?){6,14}[0-9]$',
error_messages={'invalid': phone_format},
required=True
)
token = serializers.CharField(label=_("Token"),
max_length=settings.TOTP_DIGITS,
required=True)

def validate_token(self, token):
phone = self.initial_data.get('phone')
try:
token = int(token)
device = VerificationDevice.objects.get(
unverified_phone=phone, verified=False)
except ValueError:
raise serializers.ValidationError(_("Token must be a number."))
except VerificationDevice.DoesNotExist:
raise serializers.ValidationError(
"Phone is already verified or not linked to any user account.")

user = device.user
if device.verify_token(token):
if user.phone != phone:
user.phone = phone
user.phone_verified = True
user.is_active = True
user.save()
device.delete()
return token
elif device.verify_token(token=token, tolerance=5):
raise serializers.ValidationError(
_("The token has expired."))
else:
raise serializers.ValidationError(
_("Invalid Token. Enter a valid token."))
130 changes: 130 additions & 0 deletions cadasta/accounts/tests/test_serializers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import random
import pytest

from django.conf import settings
from django.utils.translation import gettext as _
from django.test import TestCase
# from django.db import IntegrityError
from allauth.account.models import EmailAddress
from rest_framework.test import APIRequestFactory, force_authenticate
from rest_framework.request import Request
from unittest import mock

from core.messages import SANITIZE_ERROR
from core.tests.utils.cases import UserTestCase, FileStorageTestCase
Expand All @@ -30,6 +32,7 @@


class RegistrationSerializerTest(UserTestCase, TestCase):

def test_field_serialization(self):
user = UserFactory.build()
serializer = serializers.RegistrationSerializer(user)
Expand Down Expand Up @@ -503,6 +506,7 @@ def test_signup_with_existing_phone_in_VerificationDevice(self):

@pytest.mark.usefixtures('make_dirs')
class UserSerializerTest(UserTestCase, FileStorageTestCase, TestCase):

def test_field_serialization(self):
user = UserFactory.build()
serializer = serializers.UserSerializer(user)
Expand Down Expand Up @@ -912,6 +916,7 @@ def test_create_with_email_only(self):


class AccountLoginSerializerTest(UserTestCase, TestCase):

def test_unverified_account(self):
"""Serializer should raise exceptions.EmailNotVerifiedError exeception
when the user has not verified their email address"""
Expand Down Expand Up @@ -950,6 +955,7 @@ def test_login_account_with_both_unverified_phone_and_email(self):


class ChangePasswordSerializerTest(UserTestCase, TestCase):

def test_user_can_change_pw(self):
user = UserFactory.create(password='beatles4Lyfe!', change_pw=True)
request = APIRequestFactory().patch('/user/imagine71', {})
Expand Down Expand Up @@ -1108,3 +1114,127 @@ def test_password_contains_phone(self):
assert serializer.is_valid() is False
assert (_("Passwords cannot contain your phone.")
in serializer._errors['new_password'])


class PhoneVerificationSerializerTest(UserTestCase, TestCase):

def setUp(self):
super().setUp()
self.user = UserFactory.create(phone='+919327768250')

def test_valid_token_and_phone(self):
self.user.is_active = False
self.user.save()
device = VerificationDevice.objects.create(
user=self.user, unverified_phone=self.user.phone)
token = device.generate_challenge()
data = {
'phone': self.user.phone,
'token': token
}
serializer = serializers.PhoneVerificationSerializer(data=data)
assert serializer.is_valid() is True
self.user.refresh_from_db()
assert self.user.phone_verified is True
assert self.user.is_active is True
assert VerificationDevice.objects.count() == 0

def test_update_phone(self):
device = VerificationDevice.objects.create(
user=self.user, unverified_phone='+12345678990')
token = device.generate_challenge()
data = {
'phone': '+12345678990',
'token': token
}
serializer = serializers.PhoneVerificationSerializer(data=data)
assert serializer.is_valid() is True
self.user.refresh_from_db()
assert self.user.phone == '+12345678990'
assert self.user.phone_verified is True
assert VerificationDevice.objects.count() == 0

def test_invalid_token(self):
device = VerificationDevice.objects.create(
user=self.user, unverified_phone=self.user.phone)
token = device.generate_challenge()
token = str(int(token) - 1)
data = {
'phone': '+919327768250',
'token': token
}
serializer = serializers.PhoneVerificationSerializer(data=data)
assert serializer.is_valid() is False
assert(_("Invalid Token. Enter a valid token.")
in serializer._errors.get('token'))

def test_expired_token(self):
device = VerificationDevice.objects.create(
user=self.user, unverified_phone=self.user.phone)
now = 1497657600
with mock.patch('time.time', return_value=now):
token = device.generate_challenge()
data = {
'phone': self.user.phone,
'token': token
}
with mock.patch('time.time',
return_value=(now + settings.TOTP_TOKEN_VALIDITY + 1)):
serializer = serializers.PhoneVerificationSerializer(data=data)
assert serializer.is_valid() is False
assert (
_("The token has expired.") in serializer._errors.get('token'))

def test_invalid_token_format(self):
VerificationDevice.objects.create(
user=self.user, unverified_phone=self.user.phone)
data = {
'phone': '+12345678990',
'token': 'token'
}
serializer = serializers.PhoneVerificationSerializer(data=data)
assert serializer.is_valid() is False
assert(_("Token must be a number.")
in serializer._errors.get('token'))

def test_unlinked_phone(self):
device = VerificationDevice.objects.create(
user=self.user, unverified_phone=self.user.phone)
token = device.generate_challenge()
data = {
'phone': '+12345678990',
'token': token
}
serializer = serializers.PhoneVerificationSerializer(data=data)
assert serializer.is_valid() is False
assert (
_("Phone is already verified or not linked to any user account.")
in serializer._errors.get('token'))

def test_invalid_phone_format(self):
device = VerificationDevice.objects.create(
user=self.user, unverified_phone=self.user.phone)
token = device.generate_challenge()
data = {
'phone': '9327768250',
'token': token
}
serializer = serializers.PhoneVerificationSerializer(data=data)
assert serializer.is_valid() is False
assert phone_format in serializer._errors.get('phone')

data = {
'phone': '+91 9327768250',
'token': token
}
serializer = serializers.PhoneVerificationSerializer(data=data)
assert serializer.is_valid() is False
assert phone_format in serializer._errors.get('phone')

data = {
'phone': 'Test Number',
'token': token
}
serializer = serializers.PhoneVerificationSerializer(data=data)
assert serializer.is_valid() is False
assert phone_format in serializer._errors.get('phone')
8 changes: 8 additions & 0 deletions cadasta/accounts/tests/test_urls_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@


class UserUrlsTest(TestCase):

def test_account_user(self):
assert reverse(version_ns('accounts:user')) == version_url('/account/')

Expand All @@ -32,3 +33,10 @@ def test_account_password(self):

resolved = resolve(version_url('/account/password/'))
assert resolved.func.__name__ == api.SetPasswordView.__name__

def test_account_verify_phone(self):
assert (reverse(version_ns('accounts:verify_phone')) ==
version_url('/account/verify/phone/'))

resolved = resolve(version_url('/account/verify/phone/'))
assert resolved.func.__name__ == api.ConfirmPhoneView.__name__
77 changes: 77 additions & 0 deletions cadasta/accounts/tests/test_views_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from django.core import mail
from django.test import TestCase
from django.conf import settings
from unittest import mock

from allauth.account.models import EmailAddress

Expand Down Expand Up @@ -391,3 +393,78 @@ def test_change_password(self):
assert '[email protected]' in mail.outbox[0].to
self.user.refresh_from_db()
assert self.user.check_password('iloveyoko80!') is True


class ConfirmPhoneViewTest(APITestCase, UserTestCase, TestCase):
view_class = api_views.ConfirmPhoneView

def setup_models(self):
self.user = UserFactory.create(phone='+919327762850',
password='221B@bakerstreet')
self.device = VerificationDevice.objects.create(
user=self.user, unverified_phone=self.user.phone)

def test_successful_phone_verification(self):
token = self.device.generate_challenge()
data = {
'phone': self.user.phone,
'token': token
}
response = self.request(method='POST', post_data=data)
assert response.status_code == 200
assert 'Phone successfully verified.' in response.content['detail']
self.user.refresh_from_db()
assert self.user.phone_verified is True

def test_successful_new_phone_verification(self):
self.device.unverified_phone = '+12345678990'
self.device.save()
token = self.device.generate_challenge()
data = {
'phone': '+12345678990',
'token': token
}
response = self.request(method='POST', post_data=data)
assert response.status_code == 200
assert 'Phone successfully verified.' in response.content['detail']
self.user.refresh_from_db()
assert self.user.phone == '+12345678990'

def test_unsuccessful_phone_verification_invalid_token(self):
token = self.device.generate_challenge()
token = str(int(token) - 1)
data = {
'phone': self.user.phone,
'token': token
}
response = self.request(method='POST', post_data=data)
assert response.status_code == 400
assert (
"Invalid Token. Enter a valid token." in response.content['token'])

def test_unsuccessful_phone_verification_expired_token(self):
now = 1497657600
with mock.patch('time.time', return_value=now):
token = self.device.generate_challenge()
data = {
'phone': self.user.phone,
'token': token
}
with mock.patch('time.time',
return_value=(now + settings.TOTP_TOKEN_VALIDITY + 1)):
response = self.request(method='POST', post_data=data)
assert response.status_code == 400
assert (
"The token has expired." in response.content['token'])

def test_unsuccessful_phone_verification_non_existent_phone(self):
token = self.device.generate_challenge()
data = {
'phone': '+12345678990',
'token': token
}
response = self.request(method='POST', post_data=data)
assert response.status_code == 400
assert (
"Phone is already verified or not linked to any user account."
in response.content['token'])
2 changes: 2 additions & 0 deletions cadasta/accounts/urls/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@
url(r'^register/$', api.AccountRegister.as_view(), name='register'),
url(r'^login/$', api.AccountLogin.as_view(), name='login'),
url(r'^password/$', api.SetPasswordView.as_view(), name='password'),
url(r'^verify/phone/$', api.ConfirmPhoneView.as_view(),
name='verify_phone')
]
20 changes: 20 additions & 0 deletions cadasta/accounts/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from rest_framework.serializers import ValidationError
from rest_framework.response import Response
from rest_framework import status
from rest_framework.generics import GenericAPIView
from rest_framework.permissions import AllowAny

from djoser import views as djoser_views
from djoser import signals
Expand Down Expand Up @@ -122,3 +124,21 @@ def _action(self, serializer):
request=self.request._request,
user=self.request.user)
return response


class ConfirmPhoneView(GenericAPIView):
permission_classes = (AllowAny,)
serializer_class = serializers.PhoneVerificationSerializer

def post(self, request):
serializer = self.serializer_class(data=request.data)
try:
serializer.is_valid(raise_exception=True)
except ValidationError:
return Response(
data=serializer.errors,
status=status.HTTP_400_BAD_REQUEST,
)
return Response(
data={'detail': 'Phone successfully verified.'},
status=status.HTTP_200_OK)

0 comments on commit bddd68e

Please sign in to comment.