From 5f3d7f6239f69cad16c291642937d86404258cf1 Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Thu, 12 Dec 2019 21:08:42 +0330 Subject: [PATCH 01/20] ignore vscode files to commit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 894a44c..8479268 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ venv.bak/ # mypy .mypy_cache/ + +# vscode configuration +.vscode From d9468a1ba05062a6172f63aaa7964e0f7c93a673 Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Mon, 23 Dec 2019 10:33:31 +0330 Subject: [PATCH 02/20] chore: using otp method of kavenegar --- phone_verify/backends/base.py | 2 +- requirements/common.txt | 1 + requirements/testing.txt | 4 ++-- tests/factories.py | 4 ++-- tests/test_api.py | 7 +++--- tests/test_settings.py | 43 +++++++++++++++++++++++------------ 6 files changed, 37 insertions(+), 24 deletions(-) diff --git a/phone_verify/backends/base.py b/phone_verify/backends/base.py index 195f610..0bbab45 100644 --- a/phone_verify/backends/base.py +++ b/phone_verify/backends/base.py @@ -24,7 +24,7 @@ def __init__(self, **settings): self.exception_class = None @abstractmethod - def send_sms(self, numbers, message): + def send_sms(self, number, message): raise NotImplementedError() @abstractmethod diff --git a/requirements/common.txt b/requirements/common.txt index 83b7dcb..783c9f5 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -11,3 +11,4 @@ python-dotenv==0.10.0 phonenumbers==8.10.2 django-phonenumber-field==2.1.0 twilio==6.21.0 +kavenegar==1.1.2 \ No newline at end of file diff --git a/requirements/testing.txt b/requirements/testing.txt index 1ff1134..8d1d555 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -1,7 +1,7 @@ # Testing # ------------------------------------- -pytest==4.2.1 -pytest-django==3.4.4 +pytest==5.3.1 +pytest-django==3.7.0 pytest-cov==2.6.1 django-dynamic-fixture==2.0.0 diff --git a/tests/factories.py b/tests/factories.py index 6904996..acecc7f 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -7,6 +7,6 @@ def create_verification(**kwargs): - SMSVerification = apps.get_model("phone_verify", "SMSVerification") - verification = G(SMSVerification, **kwargs) + sms_verification = apps.get_model("phone_verify", "SMSVerification") + verification = G(sms_verification, **kwargs) return verification diff --git a/tests/test_api.py b/tests/test_api.py index a1837e1..584d692 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -12,13 +12,12 @@ from . import test_settings as settings from . import factories as f -pytestmark = pytest.mark.django_db +PYTESTMARK = pytest.mark.django_db SECURITY_CODE = "123456" PHONE_NUMBER = "+13478379634" SESSION_TOKEN = "phone-auth-session-token" - def test_phone_registration_sends_message(client, mocker): url = reverse("phone-register") phone_number = PHONE_NUMBER @@ -32,8 +31,8 @@ def test_phone_registration_sends_message(client, mocker): assert response.status_code == 200 assert twilio_api.called assert "session_token" in response.data - SMSVerification = apps.get_model("phone_verify", "SMSVerification") - assert SMSVerification.objects.get( + sms_verification = apps.get_model("phone_verify", "SMSVerification") + assert sms_verification.objects.get( session_token=response.data["session_token"], phone_number=phone_number ) diff --git a/tests/test_settings.py b/tests/test_settings.py index 418be71..4bc2678 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,3 +1,29 @@ +KAVENEGAR_VERIFICATION: { + "BACKEND": "phone_verify.backends.kavenegar.KavenegarBackend", + "OPTIONS": { + "API_KEY": "fake", + "SENDER": "+14755292729" + }, + "TOKEN_LENGTH": 6, + "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", + "APP_NAME": "Phone Verify", + "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only + "VERIFY_SECURITY_CODE_ONLY_ONCE": False, +} +TWILIO_VERIFICATION: { + "BACKEND": "phone_verify.backends.twilio.TwilioBackend", + "OPTIONS": { + "SID": "fake", + "SECRET": "fake", + "FROM": "+14755292729", + "SANDBOX_TOKEN": "123456", + }, + "TOKEN_LENGTH": 6, + "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", + "APP_NAME": "Phone Verify", + "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only + "VERIFY_SECURITY_CODE_ONLY_ONCE": False, +} DJANGO_SETTINGS = { "SECRET_KEY": "change-me-later", "DATABASES": {"default": {"ENGINE": "django.db.backends.sqlite3"}}, @@ -7,19 +33,6 @@ "django.contrib.contenttypes", "phone_verify", ], - # PHONE VERIFICATION - "PHONE_VERIFICATION": { - "BACKEND": "phone_verify.backends.twilio.TwilioBackend", - "OPTIONS": { - "SID": "fake", - "SECRET": "fake", - "FROM": "+14755292729", - "SANDBOX_TOKEN": "123456", - }, - "TOKEN_LENGTH": 6, - "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", - "APP_NAME": "Phone Verify", - "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only - "VERIFY_SECURITY_CODE_ONLY_ONCE": False, - }, + # PHONE VERIFICATION + "PHONE_VERIFICATION": KAVENEGAR_VERIFICATION } From a061379f86fe8bf0ea9173b42bf5872f0866e8ec Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Mon, 23 Dec 2019 10:34:04 +0330 Subject: [PATCH 03/20] chore: using otp method of kavenegar --- phone_verify/backends/kavenegar.py | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 phone_verify/backends/kavenegar.py diff --git a/phone_verify/backends/kavenegar.py b/phone_verify/backends/kavenegar.py new file mode 100644 index 0000000..30ed4ad --- /dev/null +++ b/phone_verify/backends/kavenegar.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +# Third Party Stuff +from kavenegar import KavenegarAPI, APIException, HTTPException + +# Local +from .base import BaseBackend + + +class KavenegarBackend(BaseBackend): + def __init__(self, **options): + super(KavenegarBackend, self).__init__(**options) + # Lower case it just to be sure + options = {key.lower(): value for key, value in options.items()} + self._api_key = options.get("api_key", None) + self._sender = options.get("sender", None) + + self._api = KavenegarAPI(self._api_key) + + def send_sms(self, number, message): + try: + params = { + 'receptor': number, + 'template': '', + 'token': message, + 'type': 'sms' + } + response = self._api.sms_send(params) + print(response) + except APIException as exp: + print(exp) + except HTTPException as exp: + print(exp) + + def send_bulk_sms(self, numbers, message): + try: + params = { + 'sender': self._sender, + 'receptor': numbers, + 'message': message, + } + response = self._api.sms_sendarray(params) + print(response) + except APIException as exp: + print(exp) + except HTTPException as exp: + print(exp) From 9c4b316c6cba661bfc1966427b8a9de2d7af2896 Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Sun, 29 Dec 2019 11:22:59 +0330 Subject: [PATCH 04/20] kavenegar api iranian OTP --- tests/test_services.py | 3 +-- tests/test_settings.py | 56 +++++++++++++++++++++--------------------- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/tests/test_services.py b/tests/test_services.py index 84549c9..6caf58d 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -6,8 +6,7 @@ # phone_verify Stuff from phone_verify.services import PhoneVerificationService -pytestmark = pytest.mark.django_db - +PYTESTMARK = pytest.mark.django_db def test_message_generation_and_sending_service(client, mocker): service = PhoneVerificationService(phone_number="+13478379634") diff --git a/tests/test_settings.py b/tests/test_settings.py index 4bc2678..2985c8d 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,29 +1,3 @@ -KAVENEGAR_VERIFICATION: { - "BACKEND": "phone_verify.backends.kavenegar.KavenegarBackend", - "OPTIONS": { - "API_KEY": "fake", - "SENDER": "+14755292729" - }, - "TOKEN_LENGTH": 6, - "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", - "APP_NAME": "Phone Verify", - "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only - "VERIFY_SECURITY_CODE_ONLY_ONCE": False, -} -TWILIO_VERIFICATION: { - "BACKEND": "phone_verify.backends.twilio.TwilioBackend", - "OPTIONS": { - "SID": "fake", - "SECRET": "fake", - "FROM": "+14755292729", - "SANDBOX_TOKEN": "123456", - }, - "TOKEN_LENGTH": 6, - "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", - "APP_NAME": "Phone Verify", - "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only - "VERIFY_SECURITY_CODE_ONLY_ONCE": False, -} DJANGO_SETTINGS = { "SECRET_KEY": "change-me-later", "DATABASES": {"default": {"ENGINE": "django.db.backends.sqlite3"}}, @@ -33,6 +7,32 @@ "django.contrib.contenttypes", "phone_verify", ], - # PHONE VERIFICATION - "PHONE_VERIFICATION": KAVENEGAR_VERIFICATION + # PHONE VERIFICATION + "PHONE_VERIFICATION": { + "BACKEND": "phone_verify.backends.twilio.TwilioBackend", + "OPTIONS": { + "SID": "fake", + "SECRET": "fake", + "FROM": "+14755292729", + "SANDBOX_TOKEN": "123456", + }, + "TOKEN_LENGTH": 6, + "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", + "APP_NAME": "Phone Verify", + "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only + "VERIFY_SECURITY_CODE_ONLY_ONCE": False, + }, + # KAVENEGAR VERIFICATION + "KAVENEGAR_VERIFICATION": { + "BACKEND": "phone_verify.backends.kavenegar.KavenegarBackend", + "OPTIONS": { + "API_KEY": "fake", + "SENDER": "+14755292729" + }, + "TOKEN_LENGTH": 6, + "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", + "APP_NAME": "Phone Verify", + "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only + "VERIFY_SECURITY_CODE_ONLY_ONCE": False, + } } From 5596a2e376f1518fc1ad88eedfb539da3de15618 Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Sun, 19 Jan 2020 16:49:47 +0330 Subject: [PATCH 05/20] feat(docs): Add Iranian sending message platform (Kavenegar) --- phone_verify/backends/__init__.py | 6 +- phone_verify/backends/base.py | 6 +- phone_verify/serializers.py | 58 +++---- tests/test_api.py | 263 +++++++++++++++--------------- tests/test_services.py | 11 +- tests/test_settings.py | 71 ++++---- 6 files changed, 209 insertions(+), 206 deletions(-) diff --git a/phone_verify/backends/__init__.py b/phone_verify/backends/__init__.py index fbbd5ab..3d257c7 100644 --- a/phone_verify/backends/__init__.py +++ b/phone_verify/backends/__init__.py @@ -13,8 +13,8 @@ def get_sms_backend(phone_number): if not backend: backend_import = DEFAULT_SERVICE - if settings.PHONE_VERIFICATION.get("BACKEND", None): - backend_import = settings.PHONE_VERIFICATION["BACKEND"] + if settings.PHONE_VERIFICATION['DEFAULT'].get("BACKEND", None): + backend_import = settings.PHONE_VERIFICATION['DEFAULT']["BACKEND"] backend_cls = import_string(backend_import) - return backend_cls(**settings.PHONE_VERIFICATION["OPTIONS"]) + return backend_cls(**settings.PHONE_VERIFICATION['DEFAULT']["OPTIONS"]) diff --git a/phone_verify/backends/base.py b/phone_verify/backends/base.py index 0bbab45..07d3d5f 100644 --- a/phone_verify/backends/base.py +++ b/phone_verify/backends/base.py @@ -36,7 +36,7 @@ def generate_security_code(cls): """ Returns a unique random `security_code` for given `TOKEN_LENGTH` in the settings. """ - token_length = django_settings.PHONE_VERIFICATION.get( + token_length = django_settings.PHONE_VERIFICATION['DEFAULT'].get( "TOKEN_LENGTH", DEFAULT_TOKEN_LENGTH ) return get_random_string(token_length, allowed_chars="0123456789") @@ -56,7 +56,7 @@ def check_security_code_expiry(cls, stored_verification): Returns True if the `security_code` for the `stored_verification` is expired. """ time_difference = timezone.now() - stored_verification.created_at - if time_difference.seconds > django_settings.PHONE_VERIFICATION.get( + if time_difference.seconds > django_settings.PHONE_VERIFICATION['DEFAULT'].get( "SECURITY_CODE_EXPIRATION_TIME" ): return True @@ -126,7 +126,7 @@ def validate_security_code(self, security_code, phone_number, session_token): return stored_verification, self.SECURITY_CODE_EXPIRED # check security_code is not verified - if stored_verification.is_verified and django_settings.PHONE_VERIFICATION.get( + if stored_verification.is_verified and django_settings.PHONE_VERIFICATION['DEFAULT'].get( "VERIFY_SECURITY_CODE_ONLY_ONCE" ): return stored_verification, self.SECURITY_CODE_VERIFIED diff --git a/phone_verify/serializers.py b/phone_verify/serializers.py index ec7f5e4..207b8de 100644 --- a/phone_verify/serializers.py +++ b/phone_verify/serializers.py @@ -15,35 +15,35 @@ class PhoneSerializer(serializers.Serializer): - phone_number = PhoneNumberField() + phone_number = PhoneNumberField() class SMSVerificationSerializer(serializers.Serializer): - phone_number = PhoneNumberField(required=True) - session_token = serializers.CharField(required=True) - security_code = serializers.CharField(required=True) - - def validate(self, attrs): - attrs = super().validate(attrs) - phone_number = attrs.get("phone_number", None) - security_code, session_token = ( - attrs.get("security_code", None), - attrs.get("session_token", None), - ) - backend = get_sms_backend(phone_number=phone_number) - verification, token_validatation = backend.validate_security_code( - security_code=security_code, - phone_number=phone_number, - session_token=session_token, - ) - - if verification is None: - raise serializers.ValidationError(_("Security code is not valid")) - elif token_validatation == backend.SESSION_TOKEN_INVALID: - raise serializers.ValidationError(_("Session Token mis-match")) - elif token_validatation == backend.SECURITY_CODE_EXPIRED: - raise serializers.ValidationError(_("Security code has expired")) - elif token_validatation == backend.SECURITY_CODE_VERIFIED: - raise serializers.ValidationError(_("Security code is already verified")) - - return attrs + phone_number = PhoneNumberField(required=True) + session_token = serializers.CharField(required=True) + security_code = serializers.CharField(required=True) + + def validate(self, attrs): + attrs = super().validate(attrs) + phone_number = attrs.get("phone_number", None) + security_code, session_token = ( + attrs.get("security_code", None), + attrs.get("session_token", None), + ) + backend = get_sms_backend(phone_number=phone_number) + verification, token_validatation = backend.validate_security_code( + security_code=security_code, + phone_number=phone_number, + session_token=session_token, + ) + + if verification is None: + raise serializers.ValidationError(_("Security code is not valid")) + elif token_validatation == backend.SESSION_TOKEN_INVALID: + raise serializers.ValidationError(_("Session Token mis-match")) + elif token_validatation == backend.SECURITY_CODE_EXPIRED: + raise serializers.ValidationError(_("Security code has expired")) + elif token_validatation == backend.SECURITY_CODE_VERIFIED: + raise serializers.ValidationError(_("Security code is already verified")) + + return attrs diff --git a/tests/test_api.py b/tests/test_api.py index 584d692..b0df363 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -12,157 +12,158 @@ from . import test_settings as settings from . import factories as f -PYTESTMARK = pytest.mark.django_db +pytestmark = pytest.mark.django_db SECURITY_CODE = "123456" PHONE_NUMBER = "+13478379634" SESSION_TOKEN = "phone-auth-session-token" + def test_phone_registration_sends_message(client, mocker): - url = reverse("phone-register") - phone_number = PHONE_NUMBER - data = {"phone_number": phone_number} - twilio_api = mocker.patch( - "phone_verify.services.PhoneVerificationService.send_verification" - ) + url = reverse("phone-register") + phone_number = PHONE_NUMBER + data = {"phone_number": phone_number} + twilio_api = mocker.patch( + "phone_verify.services.PhoneVerificationService.send_verification" + ) - response = client.post(url, data) + response = client.post(url, data) - assert response.status_code == 200 - assert twilio_api.called - assert "session_token" in response.data - sms_verification = apps.get_model("phone_verify", "SMSVerification") - assert sms_verification.objects.get( - session_token=response.data["session_token"], phone_number=phone_number - ) + assert response.status_code == 200 + assert twilio_api.called + assert "session_token" in response.data + sms_verification = apps.get_model("phone_verify", "SMSVerification") + assert sms_verification.objects.get( + session_token=response.data["session_token"], phone_number=phone_number + ) def test_security_code_session_token_verification_api(client): - f.create_verification( - security_code=SECURITY_CODE, - phone_number=PHONE_NUMBER, - session_token=SESSION_TOKEN, - ) - url = reverse("phone-verify") - data = { - "phone_number": PHONE_NUMBER, - "security_code": SECURITY_CODE, - "session_token": SESSION_TOKEN, - } - response = client.json.post(url, data=data) - assert response.status_code == 200 - assert response.data["message"] == "Security code is valid." + f.create_verification( + security_code=SECURITY_CODE, + phone_number=PHONE_NUMBER, + session_token=SESSION_TOKEN, + ) + url = reverse("phone-verify") + data = { + "phone_number": PHONE_NUMBER, + "security_code": SECURITY_CODE, + "session_token": SESSION_TOKEN, + } + response = client.json.post(url, data=data) + assert response.status_code == 200 + assert response.data["message"] == "Security code is valid." def test_phone_verification_with_incomplete_payload(client): - f.create_verification( - security_code=SECURITY_CODE, - phone_number=PHONE_NUMBER, - session_token=SESSION_TOKEN, - ) - url = reverse("phone-verify") - data = {"phone_number": PHONE_NUMBER} - response = client.json.post(url, data=data) - assert response.status_code == 400 - response_data = json.loads(json.dumps(response.data)) - assert response_data["session_token"][0] == "This field is required." - assert response_data["security_code"][0] == "This field is required." - - data = {"security_code": SECURITY_CODE} - response = client.json.post(url, data=data) - assert response.status_code == 400 - response_data = json.loads(json.dumps(response.data)) - assert response_data["phone_number"][0] == "This field is required." + f.create_verification( + security_code=SECURITY_CODE, + phone_number=PHONE_NUMBER, + session_token=SESSION_TOKEN, + ) + url = reverse("phone-verify") + data = {"phone_number": PHONE_NUMBER} + response = client.json.post(url, data=data) + assert response.status_code == 400 + response_data = json.loads(json.dumps(response.data)) + assert response_data["session_token"][0] == "This field is required." + assert response_data["security_code"][0] == "This field is required." + + data = {"security_code": SECURITY_CODE} + response = client.json.post(url, data=data) + assert response.status_code == 400 + response_data = json.loads(json.dumps(response.data)) + assert response_data["phone_number"][0] == "This field is required." def test_phone_verification_with_incorrect_payload(client): - f.create_verification( - security_code=SECURITY_CODE, - phone_number=PHONE_NUMBER, - session_token=SESSION_TOKEN, - ) - url = reverse("phone-verify") - # Payload with wrong session token - data = { - "phone_number": PHONE_NUMBER, - "security_code": SECURITY_CODE, - "session_token": "wrong-session-token", - } - response = client.json.post(url, data=data) - response_data = json.loads(json.dumps(response.data)) - assert response.status_code == 400 - assert response_data["non_field_errors"][0] == "Session Token mis-match" - - # Payload with wrong security code - data = { - "phone_number": PHONE_NUMBER, - "security_code": "999999", - "session_token": SESSION_TOKEN, - } - response = client.json.post(url, data=data) - assert response.status_code == 400 - response_data = json.loads(json.dumps(response.data)) - assert response_data["non_field_errors"][0] == "Security code is not valid" - - # Payload with incorrect phone_number - data = { - "phone_number": "+13478379632", - "security_code": SECURITY_CODE, - "session_token": SESSION_TOKEN, - } - response = client.json.post(url, data=data) - assert response.status_code == 400 - response_data = json.loads(json.dumps(response.data)) - assert response_data["non_field_errors"][0] == "Security code is not valid" + f.create_verification( + security_code=SECURITY_CODE, + phone_number=PHONE_NUMBER, + session_token=SESSION_TOKEN, + ) + url = reverse("phone-verify") + # Payload with wrong session token + data = { + "phone_number": PHONE_NUMBER, + "security_code": SECURITY_CODE, + "session_token": "wrong-session-token", + } + response = client.json.post(url, data=data) + response_data = json.loads(json.dumps(response.data)) + assert response.status_code == 400 + assert response_data["non_field_errors"][0] == "Session Token mis-match" + + # Payload with wrong security code + data = { + "phone_number": PHONE_NUMBER, + "security_code": "999999", + "session_token": SESSION_TOKEN, + } + response = client.json.post(url, data=data) + assert response.status_code == 400 + response_data = json.loads(json.dumps(response.data)) + assert response_data["non_field_errors"][0] == "Security code is not valid" + + # Payload with incorrect phone_number + data = { + "phone_number": "+13478379632", + "security_code": SECURITY_CODE, + "session_token": SESSION_TOKEN, + } + response = client.json.post(url, data=data) + assert response.status_code == 400 + response_data = json.loads(json.dumps(response.data)) + assert response_data["non_field_errors"][0] == "Security code is not valid" def test_check_security_code_expiry(client): - f.create_verification( - security_code=SECURITY_CODE, - phone_number=PHONE_NUMBER, - session_token=SESSION_TOKEN, - ) - time.sleep(2) - url = reverse("phone-verify") - data = { - "phone_number": PHONE_NUMBER, - "security_code": SECURITY_CODE, - "session_token": SESSION_TOKEN, - } - response = client.json.post(url, data=data) - assert response.status_code == 400 - response_data = json.loads(json.dumps(response.data)) - assert response_data["non_field_errors"][0] == "Security code has expired" + f.create_verification( + security_code=SECURITY_CODE, + phone_number=PHONE_NUMBER, + session_token=SESSION_TOKEN, + ) + time.sleep(2) + url = reverse("phone-verify") + data = { + "phone_number": PHONE_NUMBER, + "security_code": SECURITY_CODE, + "session_token": SESSION_TOKEN, + } + response = client.json.post(url, data=data) + assert response.status_code == 400 + response_data = json.loads(json.dumps(response.data)) + assert response_data["non_field_errors"][0] == "Security code has expired" def test_verified_security_code(client): - f.create_verification( - security_code=SECURITY_CODE, - phone_number=PHONE_NUMBER, - session_token=SESSION_TOKEN, - is_verified=True, - ) - url = reverse("phone-verify") - data = { - "phone_number": PHONE_NUMBER, - "security_code": SECURITY_CODE, - "session_token": SESSION_TOKEN, - } - - # Security code verification is restricted to one time - settings.DJANGO_SETTINGS["PHONE_VERIFICATION"][ - "VERIFY_SECURITY_CODE_ONLY_ONCE" - ] = True - response = client.json.post(url, data=data) - response_data = json.loads(json.dumps(response.data)) - assert response.status_code == 400 - assert response_data["non_field_errors"][0] == "Security code is already verified" - - # Security code verification is not restricted to one time - settings.DJANGO_SETTINGS["PHONE_VERIFICATION"][ - "VERIFY_SECURITY_CODE_ONLY_ONCE" - ] = False - response = client.json.post(url, data=data) - response_data = json.loads(json.dumps(response.data)) - assert response.status_code == 200 - assert response.data["message"] == "Security code is valid." + f.create_verification( + security_code=SECURITY_CODE, + phone_number=PHONE_NUMBER, + session_token=SESSION_TOKEN, + is_verified=True, + ) + url = reverse("phone-verify") + data = { + "phone_number": PHONE_NUMBER, + "security_code": SECURITY_CODE, + "session_token": SESSION_TOKEN, + } + + # Security code verification is restricted to one time + settings.DJANGO_SETTINGS["PHONE_VERIFICATION"]['DEFAULT'][ + "VERIFY_SECURITY_CODE_ONLY_ONCE" + ] = True + response = client.json.post(url, data=data) + response_data = json.loads(json.dumps(response.data)) + assert response.status_code == 400 + assert response_data["non_field_errors"][0] == "Security code is already verified" + + # Security code verification is not restricted to one time + settings.DJANGO_SETTINGS["PHONE_VERIFICATION"]['DEFAULT'][ + "VERIFY_SECURITY_CODE_ONLY_ONCE" + ] = False + response = client.json.post(url, data=data) + response_data = json.loads(json.dumps(response.data)) + assert response.status_code == 200 + assert response.data["message"] == "Security code is valid." diff --git a/tests/test_services.py b/tests/test_services.py index 6caf58d..319c1e3 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -6,11 +6,12 @@ # phone_verify Stuff from phone_verify.services import PhoneVerificationService -PYTESTMARK = pytest.mark.django_db +pytestmark = pytest.mark.django_db + def test_message_generation_and_sending_service(client, mocker): - service = PhoneVerificationService(phone_number="+13478379634") - twilio_api = mocker.patch("phone_verify.backends.twilio.TwilioBackend.send_sms") - service.send_verification("+13478379634", "123456") + service = PhoneVerificationService(phone_number="+13478379634") + twilio_api = mocker.patch("phone_verify.backends.twilio.TwilioBackend.send_sms") + service.send_verification("+13478379634", "123456") - assert twilio_api.called + assert twilio_api.called diff --git a/tests/test_settings.py b/tests/test_settings.py index 2985c8d..cac8bc1 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,38 +1,39 @@ DJANGO_SETTINGS = { - "SECRET_KEY": "change-me-later", - "DATABASES": {"default": {"ENGINE": "django.db.backends.sqlite3"}}, - "ROOT_URLCONF": "phone_verify.urls", - "INSTALLED_APPS": [ - "django.contrib.auth", - "django.contrib.contenttypes", - "phone_verify", - ], - # PHONE VERIFICATION - "PHONE_VERIFICATION": { - "BACKEND": "phone_verify.backends.twilio.TwilioBackend", - "OPTIONS": { - "SID": "fake", - "SECRET": "fake", - "FROM": "+14755292729", - "SANDBOX_TOKEN": "123456", - }, - "TOKEN_LENGTH": 6, - "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", - "APP_NAME": "Phone Verify", - "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only - "VERIFY_SECURITY_CODE_ONLY_ONCE": False, - }, - # KAVENEGAR VERIFICATION - "KAVENEGAR_VERIFICATION": { - "BACKEND": "phone_verify.backends.kavenegar.KavenegarBackend", - "OPTIONS": { - "API_KEY": "fake", - "SENDER": "+14755292729" + "SECRET_KEY": "change-me-later", + "DATABASES": {"default": {"ENGINE": "django.db.backends.sqlite3"}}, + "ROOT_URLCONF": "phone_verify.urls", + "INSTALLED_APPS": [ + "django.contrib.auth", + "django.contrib.contenttypes", + "phone_verify", + ], + # PHONE VERIFICATION + "PHONE_VERIFICATION": { + "TWILIO": { + "BACKEND": "phone_verify.backends.twilio.TwilioBackend", + "OPTIONS": { + "SID": "fake", + "SECRET": "fake", + "FROM": "+14755292729", + "SANDBOX_TOKEN": "123456", + }, + "TOKEN_LENGTH": 6, + "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", + "APP_NAME": "Phone Verify", + "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only + "VERIFY_SECURITY_CODE_ONLY_ONCE": False, }, - "TOKEN_LENGTH": 6, - "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", - "APP_NAME": "Phone Verify", - "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only - "VERIFY_SECURITY_CODE_ONLY_ONCE": False, - } + "DEFAULT": { + "BACKEND": "phone_verify.backends.kavenegar.KavenegarBackend", + "OPTIONS": { + "API_KEY": "fake", + "SENDER": "+14755292729" + }, + "TOKEN_LENGTH": 6, + "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", + "APP_NAME": "Phone Verify", + "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only + "VERIFY_SECURITY_CODE_ONLY_ONCE": False, + } + }, } From 7dcdc99c5403f856ca265023f30542e0bc5ab168 Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Sun, 19 Jan 2020 17:22:16 +0330 Subject: [PATCH 06/20] chore(*): Fix test case errors. --- phone_verify/services.py | 6 +++--- tests/test_services.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/phone_verify/services.py b/phone_verify/services.py index 5324150..f95f5c8 100644 --- a/phone_verify/services.py +++ b/phone_verify/services.py @@ -21,7 +21,7 @@ class PhoneVerificationService(object): try: - phone_settings = settings.PHONE_VERIFICATION + phone_settings = settings.PHONE_VERIFICATION['DEFAULT'] except AttributeError: raise ImproperlyConfigured("Please define PHONE_VERIFICATION in settings") @@ -44,7 +44,7 @@ def send_verification(self, number, security_code): def _generate_message(self, security_code): return self.verification_message.format( - app=settings.PHONE_VERIFICATION.get("APP_NAME", DEFAULT_APP_NAME), + app=settings.PHONE_VERIFICATION['DEFAULT'].get("APP_NAME", DEFAULT_APP_NAME), security_code=security_code, ) @@ -58,7 +58,7 @@ def _check_required_settings(self): "SECURITY_CODE_EXPIRATION_TIME", "VERIFY_SECURITY_CODE_ONLY_ONCE", } - user_settings = set(settings.PHONE_VERIFICATION.keys()) + user_settings = set(settings.PHONE_VERIFICATION['DEFAULT'].keys()) if not required_settings.issubset(user_settings): raise ImproperlyConfigured( "Please specify following settings in settings.py: {}".format( diff --git a/tests/test_services.py b/tests/test_services.py index 319c1e3..0e5fc87 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -2,6 +2,7 @@ # Third Party Stuff import pytest +from django.conf import settings # phone_verify Stuff from phone_verify.services import PhoneVerificationService @@ -11,7 +12,7 @@ def test_message_generation_and_sending_service(client, mocker): service = PhoneVerificationService(phone_number="+13478379634") - twilio_api = mocker.patch("phone_verify.backends.twilio.TwilioBackend.send_sms") + service_api = mocker.patch(f'{settings.PHONE_VERIFICATION["DEFAULT"]["BACKEND"]}.send_sms') service.send_verification("+13478379634", "123456") - assert twilio_api.called + assert service_api.called From 66590379247c2eb0592ebcc13b1afa1c2dd6804b Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Mon, 20 Jan 2020 14:26:22 +0330 Subject: [PATCH 07/20] refactor(*): Change indentions form tab to space. --- phone_verify/backends/base.py | 4 +- phone_verify/backends/kavenegar.py | 68 ++++---- phone_verify/serializers.py | 58 +++---- setup.py | 1 + tests/test_api.py | 260 ++++++++++++++--------------- tests/test_services.py | 8 +- tests/test_settings.py | 74 ++++---- 7 files changed, 237 insertions(+), 236 deletions(-) diff --git a/phone_verify/backends/base.py b/phone_verify/backends/base.py index 07d3d5f..aa7ba20 100644 --- a/phone_verify/backends/base.py +++ b/phone_verify/backends/base.py @@ -57,7 +57,7 @@ def check_security_code_expiry(cls, stored_verification): """ time_difference = timezone.now() - stored_verification.created_at if time_difference.seconds > django_settings.PHONE_VERIFICATION['DEFAULT'].get( - "SECURITY_CODE_EXPIRATION_TIME" + "SECURITY_CODE_EXPIRATION_TIME" ): return True return False @@ -127,7 +127,7 @@ def validate_security_code(self, security_code, phone_number, session_token): # check security_code is not verified if stored_verification.is_verified and django_settings.PHONE_VERIFICATION['DEFAULT'].get( - "VERIFY_SECURITY_CODE_ONLY_ONCE" + "VERIFY_SECURITY_CODE_ONLY_ONCE" ): return stored_verification, self.SECURITY_CODE_VERIFIED diff --git a/phone_verify/backends/kavenegar.py b/phone_verify/backends/kavenegar.py index 30ed4ad..a02c516 100644 --- a/phone_verify/backends/kavenegar.py +++ b/phone_verify/backends/kavenegar.py @@ -9,40 +9,40 @@ class KavenegarBackend(BaseBackend): - def __init__(self, **options): - super(KavenegarBackend, self).__init__(**options) - # Lower case it just to be sure - options = {key.lower(): value for key, value in options.items()} - self._api_key = options.get("api_key", None) - self._sender = options.get("sender", None) + def __init__(self, **options): + super(KavenegarBackend, self).__init__(**options) + # Lower case it just to be sure + options = {key.lower(): value for key, value in options.items()} + self._api_key = options.get("api_key", None) + self._sender = options.get("sender", None) - self._api = KavenegarAPI(self._api_key) + self._api = KavenegarAPI(self._api_key) - def send_sms(self, number, message): - try: - params = { - 'receptor': number, - 'template': '', - 'token': message, - 'type': 'sms' - } - response = self._api.sms_send(params) - print(response) - except APIException as exp: - print(exp) - except HTTPException as exp: - print(exp) + def send_sms(self, number, message): + try: + params = { + 'receptor': number, + 'template': '', + 'token': message, + 'type': 'sms' + } + response = self._api.sms_send(params) + print(response) + except APIException as exp: + print(exp) + except HTTPException as exp: + print(exp) - def send_bulk_sms(self, numbers, message): - try: - params = { - 'sender': self._sender, - 'receptor': numbers, - 'message': message, - } - response = self._api.sms_sendarray(params) - print(response) - except APIException as exp: - print(exp) - except HTTPException as exp: - print(exp) + def send_bulk_sms(self, numbers, message): + try: + params = { + 'sender': self._sender, + 'receptor': numbers, + 'message': message, + } + response = self._api.sms_sendarray(params) + print(response) + except APIException as exp: + print(exp) + except HTTPException as exp: + print(exp) diff --git a/phone_verify/serializers.py b/phone_verify/serializers.py index 207b8de..ec7f5e4 100644 --- a/phone_verify/serializers.py +++ b/phone_verify/serializers.py @@ -15,35 +15,35 @@ class PhoneSerializer(serializers.Serializer): - phone_number = PhoneNumberField() + phone_number = PhoneNumberField() class SMSVerificationSerializer(serializers.Serializer): - phone_number = PhoneNumberField(required=True) - session_token = serializers.CharField(required=True) - security_code = serializers.CharField(required=True) - - def validate(self, attrs): - attrs = super().validate(attrs) - phone_number = attrs.get("phone_number", None) - security_code, session_token = ( - attrs.get("security_code", None), - attrs.get("session_token", None), - ) - backend = get_sms_backend(phone_number=phone_number) - verification, token_validatation = backend.validate_security_code( - security_code=security_code, - phone_number=phone_number, - session_token=session_token, - ) - - if verification is None: - raise serializers.ValidationError(_("Security code is not valid")) - elif token_validatation == backend.SESSION_TOKEN_INVALID: - raise serializers.ValidationError(_("Session Token mis-match")) - elif token_validatation == backend.SECURITY_CODE_EXPIRED: - raise serializers.ValidationError(_("Security code has expired")) - elif token_validatation == backend.SECURITY_CODE_VERIFIED: - raise serializers.ValidationError(_("Security code is already verified")) - - return attrs + phone_number = PhoneNumberField(required=True) + session_token = serializers.CharField(required=True) + security_code = serializers.CharField(required=True) + + def validate(self, attrs): + attrs = super().validate(attrs) + phone_number = attrs.get("phone_number", None) + security_code, session_token = ( + attrs.get("security_code", None), + attrs.get("session_token", None), + ) + backend = get_sms_backend(phone_number=phone_number) + verification, token_validatation = backend.validate_security_code( + security_code=security_code, + phone_number=phone_number, + session_token=session_token, + ) + + if verification is None: + raise serializers.ValidationError(_("Security code is not valid")) + elif token_validatation == backend.SESSION_TOKEN_INVALID: + raise serializers.ValidationError(_("Session Token mis-match")) + elif token_validatation == backend.SECURITY_CODE_EXPIRED: + raise serializers.ValidationError(_("Security code has expired")) + elif token_validatation == backend.SECURITY_CODE_VERIFIED: + raise serializers.ValidationError(_("Security code is already verified")) + + return attrs diff --git a/setup.py b/setup.py index cef1020..aefabfc 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,7 @@ "phonenumbers>=8.10.2", "django-phonenumber-field>=2.1.0", "twilio>=6.21.0", + "kavenegar>=1.1.2", ], classifiers=[ "Environment :: Web Environment", diff --git a/tests/test_api.py b/tests/test_api.py index b0df363..cc020f0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -20,150 +20,150 @@ def test_phone_registration_sends_message(client, mocker): - url = reverse("phone-register") - phone_number = PHONE_NUMBER - data = {"phone_number": phone_number} - twilio_api = mocker.patch( - "phone_verify.services.PhoneVerificationService.send_verification" - ) + url = reverse("phone-register") + phone_number = PHONE_NUMBER + data = {"phone_number": phone_number} + twilio_api = mocker.patch( + "phone_verify.services.PhoneVerificationService.send_verification" + ) - response = client.post(url, data) + response = client.post(url, data) - assert response.status_code == 200 - assert twilio_api.called - assert "session_token" in response.data - sms_verification = apps.get_model("phone_verify", "SMSVerification") - assert sms_verification.objects.get( - session_token=response.data["session_token"], phone_number=phone_number - ) + assert response.status_code == 200 + assert twilio_api.called + assert "session_token" in response.data + sms_verification = apps.get_model("phone_verify", "SMSVerification") + assert sms_verification.objects.get( + session_token=response.data["session_token"], phone_number=phone_number + ) def test_security_code_session_token_verification_api(client): - f.create_verification( - security_code=SECURITY_CODE, - phone_number=PHONE_NUMBER, - session_token=SESSION_TOKEN, - ) - url = reverse("phone-verify") - data = { - "phone_number": PHONE_NUMBER, - "security_code": SECURITY_CODE, - "session_token": SESSION_TOKEN, - } - response = client.json.post(url, data=data) - assert response.status_code == 200 - assert response.data["message"] == "Security code is valid." + f.create_verification( + security_code=SECURITY_CODE, + phone_number=PHONE_NUMBER, + session_token=SESSION_TOKEN, + ) + url = reverse("phone-verify") + data = { + "phone_number": PHONE_NUMBER, + "security_code": SECURITY_CODE, + "session_token": SESSION_TOKEN, + } + response = client.json.post(url, data=data) + assert response.status_code == 200 + assert response.data["message"] == "Security code is valid." def test_phone_verification_with_incomplete_payload(client): - f.create_verification( - security_code=SECURITY_CODE, - phone_number=PHONE_NUMBER, - session_token=SESSION_TOKEN, - ) - url = reverse("phone-verify") - data = {"phone_number": PHONE_NUMBER} - response = client.json.post(url, data=data) - assert response.status_code == 400 - response_data = json.loads(json.dumps(response.data)) - assert response_data["session_token"][0] == "This field is required." - assert response_data["security_code"][0] == "This field is required." - - data = {"security_code": SECURITY_CODE} - response = client.json.post(url, data=data) - assert response.status_code == 400 - response_data = json.loads(json.dumps(response.data)) - assert response_data["phone_number"][0] == "This field is required." + f.create_verification( + security_code=SECURITY_CODE, + phone_number=PHONE_NUMBER, + session_token=SESSION_TOKEN, + ) + url = reverse("phone-verify") + data = {"phone_number": PHONE_NUMBER} + response = client.json.post(url, data=data) + assert response.status_code == 400 + response_data = json.loads(json.dumps(response.data)) + assert response_data["session_token"][0] == "This field is required." + assert response_data["security_code"][0] == "This field is required." + + data = {"security_code": SECURITY_CODE} + response = client.json.post(url, data=data) + assert response.status_code == 400 + response_data = json.loads(json.dumps(response.data)) + assert response_data["phone_number"][0] == "This field is required." def test_phone_verification_with_incorrect_payload(client): - f.create_verification( - security_code=SECURITY_CODE, - phone_number=PHONE_NUMBER, - session_token=SESSION_TOKEN, - ) - url = reverse("phone-verify") - # Payload with wrong session token - data = { - "phone_number": PHONE_NUMBER, - "security_code": SECURITY_CODE, - "session_token": "wrong-session-token", - } - response = client.json.post(url, data=data) - response_data = json.loads(json.dumps(response.data)) - assert response.status_code == 400 - assert response_data["non_field_errors"][0] == "Session Token mis-match" - - # Payload with wrong security code - data = { - "phone_number": PHONE_NUMBER, - "security_code": "999999", - "session_token": SESSION_TOKEN, - } - response = client.json.post(url, data=data) - assert response.status_code == 400 - response_data = json.loads(json.dumps(response.data)) - assert response_data["non_field_errors"][0] == "Security code is not valid" - - # Payload with incorrect phone_number - data = { - "phone_number": "+13478379632", - "security_code": SECURITY_CODE, - "session_token": SESSION_TOKEN, - } - response = client.json.post(url, data=data) - assert response.status_code == 400 - response_data = json.loads(json.dumps(response.data)) - assert response_data["non_field_errors"][0] == "Security code is not valid" + f.create_verification( + security_code=SECURITY_CODE, + phone_number=PHONE_NUMBER, + session_token=SESSION_TOKEN, + ) + url = reverse("phone-verify") + # Payload with wrong session token + data = { + "phone_number": PHONE_NUMBER, + "security_code": SECURITY_CODE, + "session_token": "wrong-session-token", + } + response = client.json.post(url, data=data) + response_data = json.loads(json.dumps(response.data)) + assert response.status_code == 400 + assert response_data["non_field_errors"][0] == "Session Token mis-match" + + # Payload with wrong security code + data = { + "phone_number": PHONE_NUMBER, + "security_code": "999999", + "session_token": SESSION_TOKEN, + } + response = client.json.post(url, data=data) + assert response.status_code == 400 + response_data = json.loads(json.dumps(response.data)) + assert response_data["non_field_errors"][0] == "Security code is not valid" + + # Payload with incorrect phone_number + data = { + "phone_number": "+13478379632", + "security_code": SECURITY_CODE, + "session_token": SESSION_TOKEN, + } + response = client.json.post(url, data=data) + assert response.status_code == 400 + response_data = json.loads(json.dumps(response.data)) + assert response_data["non_field_errors"][0] == "Security code is not valid" def test_check_security_code_expiry(client): - f.create_verification( - security_code=SECURITY_CODE, - phone_number=PHONE_NUMBER, - session_token=SESSION_TOKEN, - ) - time.sleep(2) - url = reverse("phone-verify") - data = { - "phone_number": PHONE_NUMBER, - "security_code": SECURITY_CODE, - "session_token": SESSION_TOKEN, - } - response = client.json.post(url, data=data) - assert response.status_code == 400 - response_data = json.loads(json.dumps(response.data)) - assert response_data["non_field_errors"][0] == "Security code has expired" + f.create_verification( + security_code=SECURITY_CODE, + phone_number=PHONE_NUMBER, + session_token=SESSION_TOKEN, + ) + time.sleep(2) + url = reverse("phone-verify") + data = { + "phone_number": PHONE_NUMBER, + "security_code": SECURITY_CODE, + "session_token": SESSION_TOKEN, + } + response = client.json.post(url, data=data) + assert response.status_code == 400 + response_data = json.loads(json.dumps(response.data)) + assert response_data["non_field_errors"][0] == "Security code has expired" def test_verified_security_code(client): - f.create_verification( - security_code=SECURITY_CODE, - phone_number=PHONE_NUMBER, - session_token=SESSION_TOKEN, - is_verified=True, - ) - url = reverse("phone-verify") - data = { - "phone_number": PHONE_NUMBER, - "security_code": SECURITY_CODE, - "session_token": SESSION_TOKEN, - } - - # Security code verification is restricted to one time - settings.DJANGO_SETTINGS["PHONE_VERIFICATION"]['DEFAULT'][ - "VERIFY_SECURITY_CODE_ONLY_ONCE" - ] = True - response = client.json.post(url, data=data) - response_data = json.loads(json.dumps(response.data)) - assert response.status_code == 400 - assert response_data["non_field_errors"][0] == "Security code is already verified" - - # Security code verification is not restricted to one time - settings.DJANGO_SETTINGS["PHONE_VERIFICATION"]['DEFAULT'][ - "VERIFY_SECURITY_CODE_ONLY_ONCE" - ] = False - response = client.json.post(url, data=data) - response_data = json.loads(json.dumps(response.data)) - assert response.status_code == 200 - assert response.data["message"] == "Security code is valid." + f.create_verification( + security_code=SECURITY_CODE, + phone_number=PHONE_NUMBER, + session_token=SESSION_TOKEN, + is_verified=True, + ) + url = reverse("phone-verify") + data = { + "phone_number": PHONE_NUMBER, + "security_code": SECURITY_CODE, + "session_token": SESSION_TOKEN, + } + + # Security code verification is restricted to one time + settings.DJANGO_SETTINGS["PHONE_VERIFICATION"]['DEFAULT'][ + "VERIFY_SECURITY_CODE_ONLY_ONCE" + ] = True + response = client.json.post(url, data=data) + response_data = json.loads(json.dumps(response.data)) + assert response.status_code == 400 + assert response_data["non_field_errors"][0] == "Security code is already verified" + + # Security code verification is not restricted to one time + settings.DJANGO_SETTINGS["PHONE_VERIFICATION"]['DEFAULT'][ + "VERIFY_SECURITY_CODE_ONLY_ONCE" + ] = False + response = client.json.post(url, data=data) + response_data = json.loads(json.dumps(response.data)) + assert response.status_code == 200 + assert response.data["message"] == "Security code is valid." diff --git a/tests/test_services.py b/tests/test_services.py index 0e5fc87..5a3c55f 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -11,8 +11,8 @@ def test_message_generation_and_sending_service(client, mocker): - service = PhoneVerificationService(phone_number="+13478379634") - service_api = mocker.patch(f'{settings.PHONE_VERIFICATION["DEFAULT"]["BACKEND"]}.send_sms') - service.send_verification("+13478379634", "123456") + service = PhoneVerificationService(phone_number="+13478379634") + service_api = mocker.patch(f'{settings.PHONE_VERIFICATION["DEFAULT"]["BACKEND"]}.send_sms') + service.send_verification("+13478379634", "123456") - assert service_api.called + assert service_api.called diff --git a/tests/test_settings.py b/tests/test_settings.py index cac8bc1..faa8a2e 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,39 +1,39 @@ DJANGO_SETTINGS = { - "SECRET_KEY": "change-me-later", - "DATABASES": {"default": {"ENGINE": "django.db.backends.sqlite3"}}, - "ROOT_URLCONF": "phone_verify.urls", - "INSTALLED_APPS": [ - "django.contrib.auth", - "django.contrib.contenttypes", - "phone_verify", - ], - # PHONE VERIFICATION - "PHONE_VERIFICATION": { - "TWILIO": { - "BACKEND": "phone_verify.backends.twilio.TwilioBackend", - "OPTIONS": { - "SID": "fake", - "SECRET": "fake", - "FROM": "+14755292729", - "SANDBOX_TOKEN": "123456", - }, - "TOKEN_LENGTH": 6, - "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", - "APP_NAME": "Phone Verify", - "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only - "VERIFY_SECURITY_CODE_ONLY_ONCE": False, - }, - "DEFAULT": { - "BACKEND": "phone_verify.backends.kavenegar.KavenegarBackend", - "OPTIONS": { - "API_KEY": "fake", - "SENDER": "+14755292729" - }, - "TOKEN_LENGTH": 6, - "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", - "APP_NAME": "Phone Verify", - "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only - "VERIFY_SECURITY_CODE_ONLY_ONCE": False, - } - }, + "SECRET_KEY": "change-me-later", + "DATABASES": {"default": {"ENGINE": "django.db.backends.sqlite3"}}, + "ROOT_URLCONF": "phone_verify.urls", + "INSTALLED_APPS": [ + "django.contrib.auth", + "django.contrib.contenttypes", + "phone_verify", + ], + # PHONE VERIFICATION + "PHONE_VERIFICATION": { + "TWILIO": { + "BACKEND": "phone_verify.backends.twilio.TwilioBackend", + "OPTIONS": { + "SID": "fake", + "SECRET": "fake", + "FROM": "+14755292729", + "SANDBOX_TOKEN": "123456", + }, + "TOKEN_LENGTH": 6, + "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", + "APP_NAME": "Phone Verify", + "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only + "VERIFY_SECURITY_CODE_ONLY_ONCE": False, + }, + "DEFAULT": { + "BACKEND": "phone_verify.backends.kavenegar.KavenegarBackend", + "OPTIONS": { + "API_KEY": "fake", + "SENDER": "+14755292729" + }, + "TOKEN_LENGTH": 6, + "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", + "APP_NAME": "Phone Verify", + "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only + "VERIFY_SECURITY_CODE_ONLY_ONCE": False, + } + }, } From ccbb2e9bb9b3449668b8fc2d88d2af2645940d50 Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Sat, 25 Jan 2020 10:23:43 +0330 Subject: [PATCH 08/20] chore(): Upgrade requirements --- requirements/testing.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/testing.txt b/requirements/testing.txt index 1ff1134..2d8bb20 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -1,10 +1,10 @@ # Testing # ------------------------------------- -pytest==4.2.1 -pytest-django==3.4.4 +pytest==5.3.1 +pytest-django==3.7.0 pytest-cov==2.6.1 -django-dynamic-fixture==2.0.0 +django-dynamic-fixture==3.0.2 flake8-mypy==17.8.0 pytest-mock==1.10.0 tox==3.13.2 From 6381fdc2eab8a4e5fab3cb8ef1a2821442dea7ab Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Sat, 25 Jan 2020 15:49:30 +0330 Subject: [PATCH 09/20] refactor(): Phone verification in settings consists the active backend --- README.rst | 1 + phone_verify/backends/__init__.py | 6 ++--- phone_verify/backends/base.py | 6 ++--- phone_verify/backends/kavenegar.py | 40 +++++++++--------------------- phone_verify/services.py | 6 ++--- tests/test_api.py | 21 +++++++++++++--- tests/test_services.py | 2 +- tests/test_settings.py | 37 +++++++++------------------ 8 files changed, 52 insertions(+), 67 deletions(-) diff --git a/README.rst b/README.rst index 571e669..e4926eb 100644 --- a/README.rst +++ b/README.rst @@ -83,6 +83,7 @@ Configuration "VERIFY_SECURITY_CODE_ONLY_ONCE": False, # If False, then a security code can be used multiple times for verification } +- In case of using Kavenegar as your backend service, you have to replace ``BACKEND`` with ``phone_verify.backends.kavenegar.KavenegarBackend`` and locate your ``API-KEY` in ``SECRET`` and ``SENDER`` in ``FROM``, extra fields could be omitted. Usage ----- diff --git a/phone_verify/backends/__init__.py b/phone_verify/backends/__init__.py index 3d257c7..fbbd5ab 100644 --- a/phone_verify/backends/__init__.py +++ b/phone_verify/backends/__init__.py @@ -13,8 +13,8 @@ def get_sms_backend(phone_number): if not backend: backend_import = DEFAULT_SERVICE - if settings.PHONE_VERIFICATION['DEFAULT'].get("BACKEND", None): - backend_import = settings.PHONE_VERIFICATION['DEFAULT']["BACKEND"] + if settings.PHONE_VERIFICATION.get("BACKEND", None): + backend_import = settings.PHONE_VERIFICATION["BACKEND"] backend_cls = import_string(backend_import) - return backend_cls(**settings.PHONE_VERIFICATION['DEFAULT']["OPTIONS"]) + return backend_cls(**settings.PHONE_VERIFICATION["OPTIONS"]) diff --git a/phone_verify/backends/base.py b/phone_verify/backends/base.py index aa7ba20..59fbd22 100644 --- a/phone_verify/backends/base.py +++ b/phone_verify/backends/base.py @@ -36,7 +36,7 @@ def generate_security_code(cls): """ Returns a unique random `security_code` for given `TOKEN_LENGTH` in the settings. """ - token_length = django_settings.PHONE_VERIFICATION['DEFAULT'].get( + token_length = django_settings.PHONE_VERIFICATION.get( "TOKEN_LENGTH", DEFAULT_TOKEN_LENGTH ) return get_random_string(token_length, allowed_chars="0123456789") @@ -56,7 +56,7 @@ def check_security_code_expiry(cls, stored_verification): Returns True if the `security_code` for the `stored_verification` is expired. """ time_difference = timezone.now() - stored_verification.created_at - if time_difference.seconds > django_settings.PHONE_VERIFICATION['DEFAULT'].get( + if time_difference.seconds > django_settings.PHONE_VERIFICATION.get( "SECURITY_CODE_EXPIRATION_TIME" ): return True @@ -126,7 +126,7 @@ def validate_security_code(self, security_code, phone_number, session_token): return stored_verification, self.SECURITY_CODE_EXPIRED # check security_code is not verified - if stored_verification.is_verified and django_settings.PHONE_VERIFICATION['DEFAULT'].get( + if stored_verification.is_verified and django_settings.PHONE_VERIFICATION.get( "VERIFY_SECURITY_CODE_ONLY_ONCE" ): return stored_verification, self.SECURITY_CODE_VERIFIED diff --git a/phone_verify/backends/kavenegar.py b/phone_verify/backends/kavenegar.py index a02c516..7380ebc 100644 --- a/phone_verify/backends/kavenegar.py +++ b/phone_verify/backends/kavenegar.py @@ -8,41 +8,25 @@ from .base import BaseBackend +class KavenegarException(APIException, HTTPException): + pass + + class KavenegarBackend(BaseBackend): def __init__(self, **options): super(KavenegarBackend, self).__init__(**options) # Lower case it just to be sure options = {key.lower(): value for key, value in options.items()} - self._api_key = options.get("api_key", None) - self._sender = options.get("sender", None) + self.api_key = options.get("secret", None) + self.sender = options.get("from", None) - self._api = KavenegarAPI(self._api_key) + self.client = KavenegarAPI(self.api_key) + self.exception_class = KavenegarException def send_sms(self, number, message): - try: - params = { - 'receptor': number, - 'template': '', - 'token': message, - 'type': 'sms' - } - response = self._api.sms_send(params) - print(response) - except APIException as exp: - print(exp) - except HTTPException as exp: - print(exp) + params = {'receptor': number, 'template': '', 'token': message, 'type': 'sms'} + self.client.sms_send(params) def send_bulk_sms(self, numbers, message): - try: - params = { - 'sender': self._sender, - 'receptor': numbers, - 'message': message, - } - response = self._api.sms_sendarray(params) - print(response) - except APIException as exp: - print(exp) - except HTTPException as exp: - print(exp) + params = {'sender': self.sender, 'receptor': numbers, 'message': message, } + self.client.sms_sendarray(params) diff --git a/phone_verify/services.py b/phone_verify/services.py index f95f5c8..5324150 100644 --- a/phone_verify/services.py +++ b/phone_verify/services.py @@ -21,7 +21,7 @@ class PhoneVerificationService(object): try: - phone_settings = settings.PHONE_VERIFICATION['DEFAULT'] + phone_settings = settings.PHONE_VERIFICATION except AttributeError: raise ImproperlyConfigured("Please define PHONE_VERIFICATION in settings") @@ -44,7 +44,7 @@ def send_verification(self, number, security_code): def _generate_message(self, security_code): return self.verification_message.format( - app=settings.PHONE_VERIFICATION['DEFAULT'].get("APP_NAME", DEFAULT_APP_NAME), + app=settings.PHONE_VERIFICATION.get("APP_NAME", DEFAULT_APP_NAME), security_code=security_code, ) @@ -58,7 +58,7 @@ def _check_required_settings(self): "SECURITY_CODE_EXPIRATION_TIME", "VERIFY_SECURITY_CODE_ONLY_ONCE", } - user_settings = set(settings.PHONE_VERIFICATION['DEFAULT'].keys()) + user_settings = set(settings.PHONE_VERIFICATION.keys()) if not required_settings.issubset(user_settings): raise ImproperlyConfigured( "Please specify following settings in settings.py: {}".format( diff --git a/tests/test_api.py b/tests/test_api.py index cc020f0..2f1eabd 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -23,14 +23,27 @@ def test_phone_registration_sends_message(client, mocker): url = reverse("phone-register") phone_number = PHONE_NUMBER data = {"phone_number": phone_number} - twilio_api = mocker.patch( + api = mocker.patch( "phone_verify.services.PhoneVerificationService.send_verification" ) response = client.post(url, data) assert response.status_code == 200 - assert twilio_api.called + assert api.called + assert "session_token" in response.data + sms_verification = apps.get_model("phone_verify", "SMSVerification") + assert sms_verification.objects.get( + session_token=response.data["session_token"], phone_number=phone_number + ) + + settings.DJANGO_SETTINGS["PHONE_VERIFICATION"][ + "BACKEND" + ] = 'phone_verify.backends.kavenegar.KavenegarBackend' + response = client.post(url, data) + + assert response.status_code == 200 + assert api.called assert "session_token" in response.data sms_verification = apps.get_model("phone_verify", "SMSVerification") assert sms_verification.objects.get( @@ -151,7 +164,7 @@ def test_verified_security_code(client): } # Security code verification is restricted to one time - settings.DJANGO_SETTINGS["PHONE_VERIFICATION"]['DEFAULT'][ + settings.DJANGO_SETTINGS["PHONE_VERIFICATION"][ "VERIFY_SECURITY_CODE_ONLY_ONCE" ] = True response = client.json.post(url, data=data) @@ -160,7 +173,7 @@ def test_verified_security_code(client): assert response_data["non_field_errors"][0] == "Security code is already verified" # Security code verification is not restricted to one time - settings.DJANGO_SETTINGS["PHONE_VERIFICATION"]['DEFAULT'][ + settings.DJANGO_SETTINGS["PHONE_VERIFICATION"][ "VERIFY_SECURITY_CODE_ONLY_ONCE" ] = False response = client.json.post(url, data=data) diff --git a/tests/test_services.py b/tests/test_services.py index 5a3c55f..6979535 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -12,7 +12,7 @@ def test_message_generation_and_sending_service(client, mocker): service = PhoneVerificationService(phone_number="+13478379634") - service_api = mocker.patch(f'{settings.PHONE_VERIFICATION["DEFAULT"]["BACKEND"]}.send_sms') + service_api = mocker.patch(f'{settings.PHONE_VERIFICATION["BACKEND"]}.send_sms') service.send_verification("+13478379634", "123456") assert service_api.called diff --git a/tests/test_settings.py b/tests/test_settings.py index faa8a2e..06356a6 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -9,31 +9,18 @@ ], # PHONE VERIFICATION "PHONE_VERIFICATION": { - "TWILIO": { - "BACKEND": "phone_verify.backends.twilio.TwilioBackend", - "OPTIONS": { - "SID": "fake", - "SECRET": "fake", - "FROM": "+14755292729", - "SANDBOX_TOKEN": "123456", - }, - "TOKEN_LENGTH": 6, - "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", - "APP_NAME": "Phone Verify", - "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only - "VERIFY_SECURITY_CODE_ONLY_ONCE": False, + "BACKEND": "phone_verify.backends.twilio.TwilioBackend", + "OPTIONS": { + "SID": "fake", + "SECRET": "fake", + "FROM": "+14755292729", + "SANDBOX_TOKEN": "123456", }, - "DEFAULT": { - "BACKEND": "phone_verify.backends.kavenegar.KavenegarBackend", - "OPTIONS": { - "API_KEY": "fake", - "SENDER": "+14755292729" - }, - "TOKEN_LENGTH": 6, - "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", - "APP_NAME": "Phone Verify", - "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only - "VERIFY_SECURITY_CODE_ONLY_ONCE": False, - } + "TOKEN_LENGTH": 6, + "MESSAGE": "Welcome to {app}! Please use security code {security_code} to proceed.", + "APP_NAME": "Phone Verify", + "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only + "VERIFY_SECURITY_CODE_ONLY_ONCE": False, + }, } From de56ea498e6957d6bb076aa20e7db48e0e8565f4 Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Sun, 26 Jan 2020 09:51:52 +0330 Subject: [PATCH 10/20] refactor(*): Test coverage, code improvements. --- phone_verify/backends/base.py | 4 ++-- tests/test_api.py | 11 +++++++---- tests/test_settings.py | 1 - 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/phone_verify/backends/base.py b/phone_verify/backends/base.py index 59fbd22..0bbab45 100644 --- a/phone_verify/backends/base.py +++ b/phone_verify/backends/base.py @@ -57,7 +57,7 @@ def check_security_code_expiry(cls, stored_verification): """ time_difference = timezone.now() - stored_verification.created_at if time_difference.seconds > django_settings.PHONE_VERIFICATION.get( - "SECURITY_CODE_EXPIRATION_TIME" + "SECURITY_CODE_EXPIRATION_TIME" ): return True return False @@ -127,7 +127,7 @@ def validate_security_code(self, security_code, phone_number, session_token): # check security_code is not verified if stored_verification.is_verified and django_settings.PHONE_VERIFICATION.get( - "VERIFY_SECURITY_CODE_ONLY_ONCE" + "VERIFY_SECURITY_CODE_ONLY_ONCE" ): return stored_verification, self.SECURITY_CODE_VERIFIED diff --git a/tests/test_api.py b/tests/test_api.py index 2f1eabd..20b00d2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -23,14 +23,14 @@ def test_phone_registration_sends_message(client, mocker): url = reverse("phone-register") phone_number = PHONE_NUMBER data = {"phone_number": phone_number} - api = mocker.patch( - "phone_verify.services.PhoneVerificationService.send_verification" + twilio_api = mocker.patch( + "twilio.rest.Client" ) response = client.post(url, data) assert response.status_code == 200 - assert api.called + assert twilio_api.called assert "session_token" in response.data sms_verification = apps.get_model("phone_verify", "SMSVerification") assert sms_verification.objects.get( @@ -40,10 +40,13 @@ def test_phone_registration_sends_message(client, mocker): settings.DJANGO_SETTINGS["PHONE_VERIFICATION"][ "BACKEND" ] = 'phone_verify.backends.kavenegar.KavenegarBackend' + kavenegar_api = mocker.patch( + "kavenegar.KavenegarAPI" + ) response = client.post(url, data) assert response.status_code == 200 - assert api.called + assert kavenegar_api.called assert "session_token" in response.data sms_verification = apps.get_model("phone_verify", "SMSVerification") assert sms_verification.objects.get( diff --git a/tests/test_settings.py b/tests/test_settings.py index 06356a6..418be71 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -21,6 +21,5 @@ "APP_NAME": "Phone Verify", "SECURITY_CODE_EXPIRATION_TIME": 1, # In seconds only "VERIFY_SECURITY_CODE_ONLY_ONCE": False, - }, } From 5884cde38bccda76d9f70c89d2281b98d9dc94ba Mon Sep 17 00:00:00 2001 From: sepehr hasanabadi Date: Sun, 26 Jan 2020 10:04:57 +0330 Subject: [PATCH 11/20] refactor(README): Add Kavenegar Docs to README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e4926eb..32027c4 100644 --- a/README.rst +++ b/README.rst @@ -83,7 +83,7 @@ Configuration "VERIFY_SECURITY_CODE_ONLY_ONCE": False, # If False, then a security code can be used multiple times for verification } -- In case of using Kavenegar as your backend service, you have to replace ``BACKEND`` with ``phone_verify.backends.kavenegar.KavenegarBackend`` and locate your ``API-KEY` in ``SECRET`` and ``SENDER`` in ``FROM``, extra fields could be omitted. +- In case of using Kavenegar as your backend service, you have to replace ``BACKEND`` with ``phone_verify.backends.kavenegar.KavenegarBackend`` and locate your ``API-KEY`` in ``SECRET`` and ``SENDER`` in ``FROM``, extra fields could be omitted. Usage ----- From 53c2e2748128bed2057f74f11fa627bd64e96f83 Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Mon, 27 Jan 2020 17:58:45 +0330 Subject: [PATCH 12/20] refactor(*): Phone registration test have DRY approach. --- tests/test_api.py | 44 ++++++++++++++++---------------------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 20b00d2..7f15043 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -17,41 +17,29 @@ SECURITY_CODE = "123456" PHONE_NUMBER = "+13478379634" SESSION_TOKEN = "phone-auth-session-token" +BACKEND_SERVICES = [ + ("twilio.rest.Client", "phone_verify.backends.twilio.TwilioBackend"), + ("kavenegar.KavenegarAPI", 'phone_verify.backends.kavenegar.KavenegarBackend') +] def test_phone_registration_sends_message(client, mocker): url = reverse("phone-register") phone_number = PHONE_NUMBER data = {"phone_number": phone_number} - twilio_api = mocker.patch( - "twilio.rest.Client" - ) - - response = client.post(url, data) - - assert response.status_code == 200 - assert twilio_api.called - assert "session_token" in response.data - sms_verification = apps.get_model("phone_verify", "SMSVerification") - assert sms_verification.objects.get( - session_token=response.data["session_token"], phone_number=phone_number - ) - settings.DJANGO_SETTINGS["PHONE_VERIFICATION"][ - "BACKEND" - ] = 'phone_verify.backends.kavenegar.KavenegarBackend' - kavenegar_api = mocker.patch( - "kavenegar.KavenegarAPI" - ) - response = client.post(url, data) - - assert response.status_code == 200 - assert kavenegar_api.called - assert "session_token" in response.data - sms_verification = apps.get_model("phone_verify", "SMSVerification") - assert sms_verification.objects.get( - session_token=response.data["session_token"], phone_number=phone_number - ) + for backend_service, backend in BACKEND_SERVICES: + settings.DJANGO_SETTINGS["PHONE_VERIFICATION"]["BACKEND"] = backend + api = mocker.patch(backend_service) + response = client.post(url, data) + + assert response.status_code == 200 + assert api.called + assert "session_token" in response.data + SMSVerification = apps.get_model("phone_verify", "SMSVerification") + assert SMSVerification.objects.get( + session_token=response.data["session_token"], phone_number=phone_number + ) def test_security_code_session_token_verification_api(client): From 87281a7f15a26af193fb4ea2ba8e5d236b6bef76 Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Thu, 30 Jan 2020 18:21:31 +0330 Subject: [PATCH 13/20] refactor(*): Test services for available backends, Alter exception handling for kavenegar backend. --- phone_verify/backends/kavenegar.py | 6 +----- tests/__init__.py | 4 ++++ tests/test_api.py | 9 +++------ tests/test_services.py | 13 ++++++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/phone_verify/backends/kavenegar.py b/phone_verify/backends/kavenegar.py index 7380ebc..f3a48f7 100644 --- a/phone_verify/backends/kavenegar.py +++ b/phone_verify/backends/kavenegar.py @@ -8,10 +8,6 @@ from .base import BaseBackend -class KavenegarException(APIException, HTTPException): - pass - - class KavenegarBackend(BaseBackend): def __init__(self, **options): super(KavenegarBackend, self).__init__(**options) @@ -21,7 +17,7 @@ def __init__(self, **options): self.sender = options.get("from", None) self.client = KavenegarAPI(self.api_key) - self.exception_class = KavenegarException + self.exception_class = APIException, HTTPException def send_sms(self, number, message): params = {'receptor': number, 'template': '', 'token': message, 'type': 'sms'} diff --git a/tests/__init__.py b/tests/__init__.py index 40a96af..9683240 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,5 @@ # -*- coding: utf-8 -*- +BACKEND_SERVICES = [ + ("phone_verify.backends.twilio.TwilioBackend", "twilio.rest.Client"), + ('phone_verify.backends.kavenegar.KavenegarBackend', "kavenegar.KavenegarAPI") +] \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py index 7f15043..180e39b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -11,16 +11,13 @@ from . import test_settings as settings from . import factories as f +from . import BACKEND_SERVICES pytestmark = pytest.mark.django_db SECURITY_CODE = "123456" PHONE_NUMBER = "+13478379634" SESSION_TOKEN = "phone-auth-session-token" -BACKEND_SERVICES = [ - ("twilio.rest.Client", "phone_verify.backends.twilio.TwilioBackend"), - ("kavenegar.KavenegarAPI", 'phone_verify.backends.kavenegar.KavenegarBackend') -] def test_phone_registration_sends_message(client, mocker): @@ -28,13 +25,13 @@ def test_phone_registration_sends_message(client, mocker): phone_number = PHONE_NUMBER data = {"phone_number": phone_number} - for backend_service, backend in BACKEND_SERVICES: + for backend, backend_service in BACKEND_SERVICES: settings.DJANGO_SETTINGS["PHONE_VERIFICATION"]["BACKEND"] = backend api = mocker.patch(backend_service) response = client.post(url, data) assert response.status_code == 200 - assert api.called + assert api.assert_called_once assert "session_token" in response.data SMSVerification = apps.get_model("phone_verify", "SMSVerification") assert SMSVerification.objects.get( diff --git a/tests/test_services.py b/tests/test_services.py index 6979535..ddcb61f 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -2,17 +2,20 @@ # Third Party Stuff import pytest -from django.conf import settings +from . import test_settings as settings # phone_verify Stuff from phone_verify.services import PhoneVerificationService +from . import BACKEND_SERVICES pytestmark = pytest.mark.django_db def test_message_generation_and_sending_service(client, mocker): - service = PhoneVerificationService(phone_number="+13478379634") - service_api = mocker.patch(f'{settings.PHONE_VERIFICATION["BACKEND"]}.send_sms') - service.send_verification("+13478379634", "123456") + for backend, backend_service in BACKEND_SERVICES: + settings.DJANGO_SETTINGS["PHONE_VERIFICATION"]["BACKEND"] = backend + api = mocker.patch(backend_service) + service = PhoneVerificationService(phone_number="+13478379634") + service.send_verification("+13478379634", "123456") - assert service_api.called + assert api.assert_called_once From ca4faad994a97e1fcd5e80973280a7eda4e11a9c Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Thu, 30 Jan 2020 18:33:17 +0330 Subject: [PATCH 14/20] refactor(init): Resolve flake8 error in test file __init__. --- tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index 9683240..7f7975b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,4 +2,4 @@ BACKEND_SERVICES = [ ("phone_verify.backends.twilio.TwilioBackend", "twilio.rest.Client"), ('phone_verify.backends.kavenegar.KavenegarBackend', "kavenegar.KavenegarAPI") -] \ No newline at end of file +] From 7f2e976a4b1abdf35839783dde16b63e7f7182b3 Mon Sep 17 00:00:00 2001 From: Sanyam Khurana <8039608+CuriousLearner@users.noreply.github.com> Date: Fri, 21 Feb 2020 11:24:38 +0530 Subject: [PATCH 15/20] Update to Py3 syntax --- phone_verify/backends/kavenegar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phone_verify/backends/kavenegar.py b/phone_verify/backends/kavenegar.py index f3a48f7..ff056a5 100644 --- a/phone_verify/backends/kavenegar.py +++ b/phone_verify/backends/kavenegar.py @@ -10,7 +10,7 @@ class KavenegarBackend(BaseBackend): def __init__(self, **options): - super(KavenegarBackend, self).__init__(**options) + super().__init__(**options) # Lower case it just to be sure options = {key.lower(): value for key, value in options.items()} self.api_key = options.get("secret", None) From 391e505cfaab0bf122bd53f9d0ffe72ed3682da5 Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Sat, 28 Mar 2020 13:35:42 +0430 Subject: [PATCH 16/20] fix: Modify sending sms parameters --- phone_verify/backends/kavenegar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phone_verify/backends/kavenegar.py b/phone_verify/backends/kavenegar.py index f3a48f7..36f2b71 100644 --- a/phone_verify/backends/kavenegar.py +++ b/phone_verify/backends/kavenegar.py @@ -20,7 +20,7 @@ def __init__(self, **options): self.exception_class = APIException, HTTPException def send_sms(self, number, message): - params = {'receptor': number, 'template': '', 'token': message, 'type': 'sms'} + params = {'sender': self.sender, 'receptor': number, 'message': message, } self.client.sms_send(params) def send_bulk_sms(self, numbers, message): From 987cbcaf1a7fcd6b53f6aee4320d6b4ee42c6f4a Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Sun, 17 May 2020 15:58:47 +0430 Subject: [PATCH 17/20] refactor(gitignore): add idea folder to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 8479268..7f5422d 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,8 @@ target/ # pyenv .python-version +.idea/ + # celery beat schedule file celerybeat-schedule From facc894147b5d823bc775334815700cf2b64ed16 Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Sun, 17 May 2020 16:56:14 +0430 Subject: [PATCH 18/20] fix(test): Modified kavenegar test_data --- tests/test_backends.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_backends.py b/tests/test_backends.py index c7379c6..439a0e7 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -64,9 +64,8 @@ def test_backends(client, mocker, backend): ) test_data = { "receptor": phone_number, - "template": "", - "token": message, - "type": "sms", + "message": message, + "sender": from_number, } response = client.post(url, data) From 0c45d815efba0dda987ba36889a3e4115e84ce9d Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Mon, 18 May 2020 11:45:53 +0430 Subject: [PATCH 19/20] fix(test): Modified tests, Create KavenegarException class. --- phone_verify/backends/kavenegar.py | 6 +++++- tests/test_backends.py | 3 +++ tests/test_services.py | 19 ++++++++++++------- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/phone_verify/backends/kavenegar.py b/phone_verify/backends/kavenegar.py index 7e8b567..79983a1 100644 --- a/phone_verify/backends/kavenegar.py +++ b/phone_verify/backends/kavenegar.py @@ -8,6 +8,10 @@ from .base import BaseBackend +class KavenegarException(HTTPException, APIException): + pass + + class KavenegarBackend(BaseBackend): def __init__(self, **options): super().__init__(**options) @@ -17,7 +21,7 @@ def __init__(self, **options): self.sender = options.get("from", None) self.client = KavenegarAPI(self.api_key) - self.exception_class = APIException, HTTPException + self.exception_class = KavenegarException def send_sms(self, number, message): params = {'sender': self.sender, 'receptor': number, 'message': message, } diff --git a/tests/test_backends.py b/tests/test_backends.py index ebf2ac8..ff96ed0 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -117,9 +117,12 @@ def test_send_bulk_sms(client, mocker, backend): cls_obj = backend_cls(**settings.PHONE_VERIFICATION["OPTIONS"]) mock_send_sms = mocker.patch(f"{backend_import}.send_sms") + numbers = ["+13478379634", "+13478379633", "+13478379632"] message = "Fake message" + if _get_backend_cls(backend) == "kavenegar.KavenegarBackend": + return cls_obj.send_bulk_sms(numbers, message) assert mock_send_sms.called assert mock_send_sms.call_count == 3 diff --git a/tests/test_services.py b/tests/test_services.py index 17fa854..453b955 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -12,11 +12,11 @@ # phone_verify Stuff import phone_verify.services +from phone_verify.backends.kavenegar import KavenegarException from phone_verify.services import ( PhoneVerificationService, send_security_code_and_generate_session_token, ) - from .test_backends import _get_backend_cls pytestmark = pytest.mark.django_db @@ -40,17 +40,22 @@ def test_exception_is_logged_when_raised(client, mocker, backend): mock_logger = mocker.patch("phone_verify.services.logger") backend_cls = _get_backend_cls(backend) if ( - backend_cls == "nexmo.NexmoBackend" - or backend_cls == "nexmo.NexmoSandboxBackend" + backend_cls == "nexmo.NexmoBackend" + or backend_cls == "nexmo.NexmoSandboxBackend" ): exc = ClientError() mock_send_verification.side_effect = exc elif ( - backend_cls == "twilio.TwilioBackend" - or backend_cls == "twilio.TwilioSandboxBackend" + backend_cls == "twilio.TwilioBackend" + or backend_cls == "twilio.TwilioSandboxBackend" ): exc = TwilioRestException(status=mocker.Mock(), uri=mocker.Mock()) mock_send_verification.side_effect = exc + elif ( + backend_cls == "kavenegar.KavenegarBackend" + ): + exc = KavenegarException() + mock_send_verification.side_effect = exc send_security_code_and_generate_session_token(phone_number="+13478379634") mock_logger.error.assert_called_once_with( f"Error in sending verification code to +13478379634: {exc}" @@ -70,8 +75,8 @@ def test_exception_is_raised_when_improper_settings(client): with pytest.raises(ImproperlyConfigured) as exc: PhoneVerificationService(phone_number="+13478379634") assert ( - exc.info - == "Please specify following settings in settings.py: OPTIONS, TOKEN_LENGTH" + exc.info + == "Please specify following settings in settings.py: OPTIONS, TOKEN_LENGTH" ) From 18707fd8b7aa7433081cfdb2cf3bb54a2a91e947 Mon Sep 17 00:00:00 2001 From: Sepehr Hasanabadi Date: Tue, 19 May 2020 17:00:52 +0430 Subject: [PATCH 20/20] fix: modified test kavenegar, Add Kavenegar sandbox --- phone_verify/backends/kavenegar.py | 40 ++++++++++++++++++++++++++---- tests/test_backends.py | 25 +++++++++++-------- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/phone_verify/backends/kavenegar.py b/phone_verify/backends/kavenegar.py index 79983a1..3286ad5 100644 --- a/phone_verify/backends/kavenegar.py +++ b/phone_verify/backends/kavenegar.py @@ -5,6 +5,7 @@ from kavenegar import KavenegarAPI, APIException, HTTPException # Local +from phone_verify.models import SMSVerification from .base import BaseBackend @@ -17,16 +18,45 @@ def __init__(self, **options): super().__init__(**options) # Lower case it just to be sure options = {key.lower(): value for key, value in options.items()} - self.api_key = options.get("secret", None) - self.sender = options.get("from", None) + self._api_key = options.get("secret", None) + self._sender = options.get("from", None) - self.client = KavenegarAPI(self.api_key) + self.client = KavenegarAPI(self._api_key) self.exception_class = KavenegarException def send_sms(self, number, message): - params = {'sender': self.sender, 'receptor': number, 'message': message, } + params = {'sender': self._sender, 'receptor': number, 'message': message, } self.client.sms_send(params) def send_bulk_sms(self, numbers, message): - params = {'sender': self.sender, 'receptor': numbers, 'message': message, } + params = {'sender': self._sender, 'receptor': numbers, 'message': message, } self.client.sms_sendarray(params) + + +class KavenegarSandboxBackend(BaseBackend): + def __init__(self, **options): + super().__init__(**options) + # Lower case it just to be sure + options = {key.lower(): value for key, value in options.items()} + self._api_key = options.get("secret", None) + self._sender = options.get("from", None) + + self.client = KavenegarAPI(self._api_key) + self.exception_class = KavenegarException + + def send_sms(self, number, message): + params = {'sender': self._sender, 'receptor': number, 'message': message, } + self.client.sms_send(params) + + def send_bulk_sms(self, numbers, message): + params = {'sender': self._sender, 'receptor': numbers, 'message': message, } + self.client.sms_sendarray(params) + + def generate_security_code(self): + """ + Returns a fixed security code + """ + return self._token + + def validate_security_code(self, security_code, phone_number, session_token): + return SMSVerification.objects.none(), self.SECURITY_CODE_VALID diff --git a/tests/test_backends.py b/tests/test_backends.py index ff96ed0..d1252f7 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -122,17 +122,20 @@ def test_send_bulk_sms(client, mocker, backend): message = "Fake message" if _get_backend_cls(backend) == "kavenegar.KavenegarBackend": - return - cls_obj.send_bulk_sms(numbers, message) - assert mock_send_sms.called - assert mock_send_sms.call_count == 3 - mock_send_sms.assert_has_calls( - [ - mocker.call(number=numbers[0], message=message), - mocker.call(number=numbers[1], message=message), - mocker.call(number=numbers[2], message=message), - ] - ) + mock_sendarray_sms = mocker.patch('kavenegar.KavenegarAPI.sms_sendarray') + cls_obj.send_bulk_sms(numbers, message) + assert mock_sendarray_sms.called + else: + cls_obj.send_bulk_sms(numbers, message) + assert mock_send_sms.called + assert mock_send_sms.call_count == 3 + mock_send_sms.assert_has_calls( + [ + mocker.call(number=numbers[0], message=message), + mocker.call(number=numbers[1], message=message), + mocker.call(number=numbers[2], message=message), + ] + ) class TestBaseBackend(BaseBackend):