diff --git a/.gitignore b/.gitignore index 894a44c..7f5422d 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,8 @@ target/ # pyenv .python-version +.idea/ + # celery beat schedule file celerybeat-schedule @@ -102,3 +104,6 @@ venv.bak/ # mypy .mypy_cache/ + +# vscode configuration +.vscode diff --git a/README.rst b/README.rst index 4bc607d..3115e95 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/conftest.py b/conftest.py index f225321..3d20364 100644 --- a/conftest.py +++ b/conftest.py @@ -16,7 +16,7 @@ from tests import test_settings -backends = {"twilio.TwilioBackend", "nexmo.NexmoBackend"} +backends = {"twilio.TwilioBackend", "nexmo.NexmoBackend", "kavenegar.KavenegarBackend"} sandbox_backends = {"twilio.TwilioSandboxBackend", "nexmo.NexmoSandboxBackend"} all_backends = list(backends) + list(sandbox_backends) diff --git a/phone_verify/backends/base.py b/phone_verify/backends/base.py index b286f7b..e6d92f3 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/phone_verify/backends/kavenegar.py b/phone_verify/backends/kavenegar.py new file mode 100644 index 0000000..3286ad5 --- /dev/null +++ b/phone_verify/backends/kavenegar.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +# Third Party Stuff +from kavenegar import KavenegarAPI, APIException, HTTPException + +# Local +from phone_verify.models import SMSVerification +from .base import BaseBackend + + +class KavenegarException(HTTPException, APIException): + pass + + +class KavenegarBackend(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) + + +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/requirements/common.txt b/requirements/common.txt index b5028df..6c89d37 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -11,4 +11,5 @@ python-dotenv==0.10.0 phonenumbers==8.10.2 django-phonenumber-field==2.1.0 twilio==6.21.0 +kavenegar==1.1.2 nexmo==2.4.0 diff --git a/setup.py b/setup.py index 8bc8568..72ec734 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", "nexmo>=2.4.0", ], classifiers=[ diff --git a/tests/factories.py b/tests/factories.py index 84d1507..1321439 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -6,6 +6,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 f7e3142..94d20a9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -25,14 +25,14 @@ def test_phone_registration_sends_message(client, mocker, backend): url = reverse("phone-register") phone_number = PHONE_NUMBER data = {"phone_number": phone_number} - twilio_api = mocker.patch( + + mock_api = mocker.patch( "phone_verify.services.PhoneVerificationService.send_verification" ) - response = client.post(url, data) assert response.status_code == 200 - assert twilio_api.called + assert mock_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_backends.py b/tests/test_backends.py index c152466..d1252f7 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -58,6 +58,16 @@ def test_backends(client, mocker, backend): "phone_verify.backends.twilio.TwilioRestClient.messages" ) mock_twilio_send_message.create = mocker.MagicMock() + elif backend_cls == "kavenegar.KavenegarBackend": + # Mock the Kavenegar client + mock_kavenegar_send_message = mocker.patch( + "phone_verify.backends.kavenegar.KavenegarAPI.sms_send" + ) + test_data = { + "receptor": phone_number, + "message": message, + "sender": from_number, + } response = client.post(url, data) assert response.status_code == 200 @@ -68,6 +78,8 @@ def test_backends(client, mocker, backend): mock_twilio_send_message.create.assert_called_once_with( to=phone_number, body=message, from_=from_number ) + elif backend_cls == "kavenegar.KavenegarBackend": + mock_kavenegar_send_message.assert_called_once_with(test_data) # Get the last part of the backend and check if that is a Sandbox Backend if backend_cls in sandbox_backends: @@ -105,19 +117,25 @@ 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" - 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), - ] - ) + if _get_backend_cls(backend) == "kavenegar.KavenegarBackend": + 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): 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" )