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

Map country code to different Twilio resources #1976

Merged
merged 16 commits into from
May 25, 2023
Merged
Prev Previous commit
Next Next commit
Add tests, lint
  • Loading branch information
mderynck committed May 23, 2023
commit 352bf51f4aa46f85a863ad288cbf2dc2b344ecef
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 3.2.18 on 2023-05-19 01:58
# Generated by Django 3.2.18 on 2023-05-23 18:59

from django.db import migrations, models
import django.db.models.deletion
Expand Down Expand Up @@ -28,7 +28,6 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(default='Default', max_length=100)),
('country_code_name', models.CharField(default='Default', max_length=100)),
('country_code', models.CharField(default=None, max_length=16, null=True)),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='twilio_sender', to='twilioapp.twilioaccount')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_twilioapp.twiliosender_set+', to='contenttypes.contenttype')),
Expand Down
2 changes: 1 addition & 1 deletion engine/apps/twilioapp/models/twilio_sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def get_twilio_api_client(self):

class TwilioSender(PolymorphicModel):
name = models.CharField(max_length=100, null=False, default="Default")
country_code_name = models.CharField(max_length=100, null=False, default="Default")
# Note: country_code does not have + prefix here
country_code = models.CharField(max_length=16, null=True, default=None)
account = models.ForeignKey("twilioapp.TwilioAccount", on_delete=models.CASCADE, related_name="twilio_sender")

Expand Down
40 changes: 40 additions & 0 deletions engine/apps/twilioapp/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest

from apps.twilioapp.tests.factories import (
TwilioAccountFactory,
TwilioPhoneCallSenderFactory,
TwilioSmsSenderFactory,
TwilioVerificationSenderFactory,
)


@pytest.fixture
def make_twilio_account():
def _make_twilio_account(**kwargs):
return TwilioAccountFactory(**kwargs)

return _make_twilio_account


@pytest.fixture
def make_twilio_phone_call_sender():
def _make_twilio_phone_call_sender(**kwargs):
return TwilioPhoneCallSenderFactory(**kwargs)

return _make_twilio_phone_call_sender


@pytest.fixture
def make_twilio_sms_sender():
def _make_twilio_sms_sender(**kwargs):
return TwilioSmsSenderFactory(**kwargs)

return _make_twilio_sms_sender


@pytest.fixture
def make_twilio_verification_sender():
def _make_twilio_verification_sender(**kwargs):
return TwilioVerificationSenderFactory(**kwargs)

return _make_twilio_verification_sender
26 changes: 26 additions & 0 deletions engine/apps/twilioapp/tests/factories.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import factory

from apps.twilioapp.models import PhoneCall, SMSMessage
from apps.twilioapp.models.twilio_sender import (
TwilioAccount,
TwilioPhoneCallSender,
TwilioSmsSender,
TwilioVerificationSender,
)


class PhoneCallFactory(factory.DjangoModelFactory):
Expand All @@ -11,3 +17,23 @@ class Meta:
class SMSFactory(factory.DjangoModelFactory):
class Meta:
model = SMSMessage


class TwilioAccountFactory(factory.DjangoModelFactory):
class Meta:
model = TwilioAccount


class TwilioPhoneCallSenderFactory(factory.DjangoModelFactory):
class Meta:
model = TwilioPhoneCallSender


class TwilioSmsSenderFactory(factory.DjangoModelFactory):
class Meta:
model = TwilioSmsSender


class TwilioVerificationSenderFactory(factory.DjangoModelFactory):
class Meta:
model = TwilioVerificationSender
147 changes: 147 additions & 0 deletions engine/apps/twilioapp/tests/test_senders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from unittest.mock import patch

import pytest
from django.conf import settings

from apps.twilioapp.twilio_client import twilio_client

US_VERIFY = "us_verify"
US_SMS = "us_sms"
US_PHONE = "us_phone"

DB_DEFAULT_VERIFY = "db_default_verify"
DB_DEFAULT_SMS = "db_default_sms"
DB_DEFAULT_PHONE = "db_default_phone"

DB_TWILIO_AUTH_TOKEN = "db_twilio_account_auth_token"
DB_TWILIO_ACCOUNT_SID = "db_twilio_account_sid"

ENV_VERIFY_SERVICE_SID = "env_twilio_verify_service_sid"
ENV_TWILIO_NUMBER = "env_twilio_number"
ENV_TWILIO_AUTH_TOKEN = "env_twilio_auth_token"
ENV_TWILIO_ACCOUNT_SID = "env_twilio_account_sid"


@pytest.fixture
def setup_env_default_twilio():
settings.TWILIO_ACCOUNT_SID = ENV_TWILIO_ACCOUNT_SID
settings.TWILIO_AUTH_TOKEN = ENV_TWILIO_AUTH_TOKEN
settings.TWILIO_NUMBER = ENV_TWILIO_NUMBER
settings.TWILIO_VERIFY_SERVICE_SID = ENV_VERIFY_SERVICE_SID


@pytest.fixture
def setup_db_default_account(make_twilio_account):
return make_twilio_account(
name="DB Twilio Account", account_sid=DB_TWILIO_ACCOUNT_SID, auth_token=DB_TWILIO_AUTH_TOKEN
)


@pytest.fixture
def setup_default_senders(
setup_db_default_account, make_twilio_phone_call_sender, make_twilio_sms_sender, make_twilio_verification_sender
):
make_twilio_phone_call_sender(name="Default", number=DB_DEFAULT_PHONE, account=setup_db_default_account)
make_twilio_sms_sender(name="Default", sender=DB_DEFAULT_SMS, account=setup_db_default_account)
make_twilio_verification_sender(
name="Default", verify_service_sid=DB_DEFAULT_VERIFY, account=setup_db_default_account
)


@pytest.fixture
def setup_us_senders(
setup_db_default_account, make_twilio_phone_call_sender, make_twilio_sms_sender, make_twilio_verification_sender
):
make_twilio_phone_call_sender(name="US/Canada", country_code="1", number=US_PHONE, account=setup_db_default_account)
make_twilio_sms_sender(name="US/Canada", country_code="1", sender=US_SMS, account=setup_db_default_account)
make_twilio_verification_sender(
name="US/Canada", country_code="1", verify_service_sid=US_VERIFY, account=setup_db_default_account
)


@pytest.mark.django_db
@pytest.mark.parametrize(
"sender,expected_from",
[
(twilio_client._phone_sender, ENV_TWILIO_NUMBER),
(twilio_client._sms_sender, ENV_TWILIO_NUMBER),
(twilio_client._verify_sender, ENV_VERIFY_SERVICE_SID),
],
)
def test_use_env_default_senders(
setup_env_default_twilio,
setup_us_senders,
make_twilio_account,
make_twilio_phone_call_sender,
make_twilio_sms_sender,
make_twilio_verification_sender,
sender,
expected_from,
):
with patch(
"apps.twilioapp.twilio_client.TwilioClient.parse_number",
return_value=(True, None, "44"),
):
client, _from = sender("")
assert _from == expected_from
assert client.username == ENV_TWILIO_ACCOUNT_SID
assert client.password == ENV_TWILIO_AUTH_TOKEN


@pytest.mark.django_db
@pytest.mark.parametrize(
"sender,expected_from",
[
(twilio_client._phone_sender, DB_DEFAULT_PHONE),
(twilio_client._sms_sender, DB_DEFAULT_SMS),
(twilio_client._verify_sender, DB_DEFAULT_VERIFY),
],
)
def test_use_db_default_senders(
setup_env_default_twilio,
setup_default_senders,
make_twilio_account,
make_twilio_phone_call_sender,
make_twilio_sms_sender,
make_twilio_verification_sender,
sender,
expected_from,
):
with patch(
"apps.twilioapp.twilio_client.TwilioClient.parse_number",
return_value=(True, None, "44"),
):
client, _from = sender("")
assert _from == expected_from
assert client.username == DB_TWILIO_ACCOUNT_SID
assert client.password == DB_TWILIO_AUTH_TOKEN


@pytest.mark.django_db
@pytest.mark.parametrize(
"sender,expected_from",
[
(twilio_client._phone_sender, US_PHONE),
(twilio_client._sms_sender, US_SMS),
(twilio_client._verify_sender, US_VERIFY),
],
)
def test_use_country_code_senders(
setup_env_default_twilio,
setup_default_senders,
setup_us_senders,
make_twilio_account,
make_twilio_phone_call_sender,
make_twilio_sms_sender,
make_twilio_verification_sender,
sender,
expected_from,
):
with patch(
"apps.twilioapp.twilio_client.TwilioClient.parse_number",
return_value=(True, None, "1"),
):
client, _from = sender("")
assert _from == expected_from
assert client.username == DB_TWILIO_ACCOUNT_SID
assert client.password == DB_TWILIO_AUTH_TOKEN
33 changes: 16 additions & 17 deletions engine/apps/twilioapp/twilio_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def default_twilio_api_client(self):
def default_twilio_number(self):
return live_settings.TWILIO_NUMBER

def twilio_sender(self, sender_type, to, accessor):
def _twilio_sender(self, sender_type, to):
_, _, country_code = self.parse_number(to)
TwilioSender = apps.get_model("twilioapp", "TwilioSender")
sender = (
Expand All @@ -39,24 +39,23 @@ def twilio_sender(self, sender_type, to, accessor):
.order_by("-country_code")
.first()
)
client = sender.account.get_twilio_api_client() if sender else self.default_twilio_api_client
return client, sender

client = self.default_twilio_api_client
if sender:
client = sender.account.get_twilio_api_client()
def _sms_sender(self, to):
client, sender = self._twilio_sender(TwilioSmsSender, to)
return client, sender.sender if sender else self.default_twilio_number

return client, accessor(sender)
def _phone_sender(self, to):
client, sender = self._twilio_sender(TwilioPhoneCallSender, to)
return client, sender.number if sender else self.default_twilio_number

def sms_accessor(self, sender):
return sender.sender if sender else self.default_twilio_number

def phone_accessor(self, sender):
return sender.number if sender else self.default_twilio_number

def verify_accessor(self, sender):
return sender.verify_service_sid if sender else live_settings.TWILIO_VERIFY_SERVICE_SID
def _verify_sender(self, to):
client, sender = self._twilio_sender(TwilioVerificationSender, to)
return client, sender.verify_service_sid if sender else live_settings.TWILIO_VERIFY_SERVICE_SID

def send_message(self, body, to):
client, from_ = self.twilio_sender(TwilioSmsSender, to, self.sms_accessor)
client, from_ = self._sms_sender(to)
status_callback = create_engine_url(reverse("twilioapp:sms_status_events"))
try:
return client.messages.create(body=body, to=to, from_=from_, status_callback=status_callback)
Expand Down Expand Up @@ -85,7 +84,7 @@ def parse_number(self, number):

def verification_start_via_twilio(self, user, phone_number, via):
# https://www.twilio.com/docs/verify/api/verification?code-sample=code-start-a-verification-with-sms&code-language=Python&code-sdk-version=6.x
client, verify_service_sid = self.twilio_sender(TwilioVerificationSender, phone_number, self.verify_accessor)
client, verify_service_sid = self._verify_sender(phone_number)
verification = None
try:
verification = client.verify.services(verify_service_sid).verifications.create(to=phone_number, channel=via)
Expand Down Expand Up @@ -117,7 +116,7 @@ def verification_start_via_twilio(self, user, phone_number, via):

def verification_check_via_twilio(self, user, phone_number, code):
# https://www.twilio.com/docs/verify/api/verification-check?code-sample=code-check-a-verification-with-a-phone-number&code-language=Python&code-sdk-version=6.x
client, verify_service_sid = self.twilio_sender(TwilioVerificationSender, phone_number, self.verify_accessor)
client, verify_service_sid = self._verify_sender(phone_number)
succeed = False
try:
verification_check = client.verify.services(verify_service_sid).verification_checks.create(
Expand Down Expand Up @@ -158,7 +157,7 @@ def make_test_call(self, to):
self.make_call(message=message, to=to)

def make_call(self, message, to, grafana_cloud=False):
client, number = self.twilio_sender(TwilioPhoneCallSender, to, self.phone_accessor)
client, number = self._phone_sender(to)
try:
start_message = message.replace('"', "")

Expand Down