Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API endpoint for phone verification(2) #1748

Merged
merged 5 commits into from
Aug 29, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)