From 85d92b6177dc49adb15ac088205d762a510d2137 Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Wed, 22 Aug 2018 20:47:53 +0200 Subject: [PATCH 01/12] Update to twilio 6.16.2 --- pip-freeze.txt | 5 +- pip-requires.txt | 2 +- temba/channels/handlers.py | 6 +-- temba/channels/models.py | 5 +- temba/channels/types/twilio/tests.py | 6 +-- temba/channels/types/twilio/type.py | 2 +- temba/channels/types/twilio/views.py | 2 +- .../types/twilio_messaging_service/tests.py | 4 +- .../types/twilio_messaging_service/views.py | 2 +- temba/channels/types/twiml_api/tests.py | 2 +- temba/channels/views.py | 2 +- temba/contacts/tests.py | 2 +- temba/ivr/clients.py | 16 +++--- temba/ivr/tests.py | 51 ++++++++++--------- temba/orgs/tests.py | 4 +- temba/orgs/views.py | 10 ++-- temba/tests/twilio.py | 45 +++++++++++----- temba/utils/twilio.py | 45 +++------------- 18 files changed, 100 insertions(+), 111 deletions(-) diff --git a/pip-freeze.txt b/pip-freeze.txt index 651a95b3f52..1b7fee4e257 100644 --- a/pip-freeze.txt +++ b/pip-freeze.txt @@ -54,7 +54,6 @@ future==0.16.0 # via django-hamlpy, python-telegram-bot geojson==1.3.5 google==1.9.3 gunicorn==19.7.1 -httplib2==0.10.3 # via twilio idna==2.5 # via cryptography, requests inflection==0.3.1 # via python-intercom ipaddress==1.0.19 # via elasticsearch-dsl @@ -94,7 +93,7 @@ pyexcel-xlsx==0.4.0 pyexcel==0.5.0 # via pyexcel-webio pyfcm==1.3.1 pyflakes==1.5.0 # via flake8 -pyjwt[crypto]==1.6.4 # via nexmo +pyjwt[crypto]==1.6.4 # via nexmo, twilio pysocks==1.6.8 ; python_version >= "3.0" python-dateutil==2.2 python-gcm==0.4 @@ -121,7 +120,7 @@ stop-words==2015.2.23.1 stripe==1.59.0 texttable==0.9.0 # via pyexcel toml==0.9.4 # via black -twilio==5.7.0 +twilio==6.16.2 twython==3.5.0 unidecode==0.4.20 urllib3==1.21.1 # via elasticsearch, requests diff --git a/pip-requires.txt b/pip-requires.txt index f1e9975e0fc..4b2df5d68a7 100644 --- a/pip-requires.txt +++ b/pip-requires.txt @@ -28,7 +28,7 @@ python-gcm python-telegram-bot raven stripe -twilio<6.0.0 +twilio twython elasticsearch elasticsearch_dsl diff --git a/temba/channels/handlers.py b/temba/channels/handlers.py index 873018ee7fa..b5b000ed969 100644 --- a/temba/channels/handlers.py +++ b/temba/channels/handlers.py @@ -3,7 +3,7 @@ from datetime import datetime import pytz -from twilio import twiml +from twilio.twiml.voice_response import VoiceResponse from django.conf import settings from django.db.models import Q @@ -80,7 +80,7 @@ def get(self, request, *args, **kwargs): # pragma: no cover return HttpResponse("ILLEGAL METHOD") def post(self, request, *args, **kwargs): - from twilio.util import RequestValidator + from twilio.request_validator import RequestValidator from temba.flows.models import FlowSession signature = request.META.get("HTTP_X_TWILIO_SIGNATURE", "") @@ -104,7 +104,7 @@ def post(self, request, *args, **kwargs): # find a channel that knows how to answer twilio calls channel = self.get_ringing_channel(uuid=channel_uuid) if not channel: - response = twiml.Response() + response = VoiceResponse() response.say("Sorry, there is no channel configured to take this call. Goodbye.") response.hangup() return HttpResponse(str(response)) diff --git a/temba/channels/models.py b/temba/channels/models.py index 911f26fd440..996e7bc1829 100644 --- a/temba/channels/models.py +++ b/temba/channels/models.py @@ -15,7 +15,8 @@ from phonenumbers import NumberParseException from pyfcm import FCMNotification from smartmin.models import SmartModel -from twilio import TwilioRestException, twiml +from twilio.base.exceptions import TwilioRestException +from twilio.twiml.voice_response import VoiceResponse from django.conf import settings from django.conf.urls import url @@ -818,7 +819,7 @@ def is_delegate_caller(self): def generate_ivr_response(self): ivr_protocol = Channel.get_type_from_code(self.channel_type).ivr_protocol if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML: - return twiml.Response() + return VoiceResponse() if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO: return NCCOResponse() diff --git a/temba/channels/types/twilio/tests.py b/temba/channels/types/twilio/tests.py index 8cec0efc88a..feb3acdf375 100644 --- a/temba/channels/types/twilio/tests.py +++ b/temba/channels/types/twilio/tests.py @@ -1,6 +1,6 @@ from mock import patch -from twilio import TwilioRestException +from twilio.base.exceptions import TwilioRestException from django.urls import reverse @@ -12,7 +12,7 @@ class TwilioTypeTest(TembaTest): @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_claim(self): self.login(self.admin) @@ -209,7 +209,7 @@ def test_claim(self): ) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_deactivate(self): # make our channel of the twilio ilk diff --git a/temba/channels/types/twilio/type.py b/temba/channels/types/twilio/type.py index 4665bfeaa74..2224fc04c40 100644 --- a/temba/channels/types/twilio/type.py +++ b/temba/channels/types/twilio/type.py @@ -1,5 +1,5 @@ -from twilio import TwilioRestException +from twilio.base.exceptions import TwilioRestException from django.utils.translation import ugettext_lazy as _ diff --git a/temba/channels/types/twilio/views.py b/temba/channels/types/twilio/views.py index f164300fe4d..f6d643a7935 100644 --- a/temba/channels/types/twilio/views.py +++ b/temba/channels/types/twilio/views.py @@ -4,7 +4,7 @@ import phonenumbers from phonenumbers.phonenumberutil import region_code_for_number from smartmin.views import SmartFormView -from twilio import TwilioRestException +from twilio.base.exceptions import TwilioRestException from django import forms from django.conf import settings diff --git a/temba/channels/types/twilio_messaging_service/tests.py b/temba/channels/types/twilio_messaging_service/tests.py index 81ccce25f31..2c4fd57fa3c 100644 --- a/temba/channels/types/twilio_messaging_service/tests.py +++ b/temba/channels/types/twilio_messaging_service/tests.py @@ -1,6 +1,6 @@ from mock import patch -from twilio import TwilioRestException +from twilio.base.exceptions import TwilioRestException from django.urls import reverse @@ -12,7 +12,7 @@ class TwilioMessagingServiceTypeTest(TembaTest): @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_claim(self): self.login(self.admin) diff --git a/temba/channels/types/twilio_messaging_service/views.py b/temba/channels/types/twilio_messaging_service/views.py index 1f7eebaa444..109d816e783 100644 --- a/temba/channels/types/twilio_messaging_service/views.py +++ b/temba/channels/types/twilio_messaging_service/views.py @@ -1,6 +1,6 @@ from smartmin.views import SmartFormView -from twilio import TwilioRestException +from twilio.base.exceptions import TwilioRestException from django import forms from django.http import HttpResponseRedirect diff --git a/temba/channels/types/twiml_api/tests.py b/temba/channels/types/twiml_api/tests.py index bcb09e283b0..1c620defdfe 100644 --- a/temba/channels/types/twiml_api/tests.py +++ b/temba/channels/types/twiml_api/tests.py @@ -9,7 +9,7 @@ class TwimlAPITypeTest(TembaTest): @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_claim(self): self.login(self.admin) diff --git a/temba/channels/views.py b/temba/channels/views.py index 512ad8e8424..dad3d5b5e1c 100644 --- a/temba/channels/views.py +++ b/temba/channels/views.py @@ -20,7 +20,7 @@ SmartTemplateView, SmartUpdateView, ) -from twilio import TwilioRestException +from twilio.base.exceptions import TwilioRestException from django import forms from django.conf import settings diff --git a/temba/contacts/tests.py b/temba/contacts/tests.py index e5de1616281..e9b9cd6f138 100644 --- a/temba/contacts/tests.py +++ b/temba/contacts/tests.py @@ -1043,7 +1043,7 @@ def test_contact_send_all(self): self.assertIsNotNone(out_msgs.filter(contact_urn__path="+12078778899").first()) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) @override_settings(SEND_CALLS=True) def test_release(self): diff --git a/temba/ivr/clients.py b/temba/ivr/clients.py index 48478b29945..cc731afe07b 100644 --- a/temba/ivr/clients.py +++ b/temba/ivr/clients.py @@ -3,8 +3,8 @@ import requests from nexmo import AuthenticationError, ClientError, ServerError -from twilio import TwilioRestException -from twilio.util import RequestValidator +from twilio.base.exceptions import TwilioRestException +from twilio.request_validator import RequestValidator from django.conf import settings from django.urls import reverse @@ -126,9 +126,9 @@ def hangup(self, call): class TwilioClient(TembaTwilioRestClient): - def __init__(self, account, token, org, **kwargs): + def __init__(self, account_sid, token, org, **kwargs): self.org = org - super().__init__(account=account, token=token, **kwargs) + super().__init__(account_sid, token, **kwargs) def start_call(self, call, to, from_, status_callback): if not settings.SEND_CALLS: @@ -144,7 +144,7 @@ def start_call(self, call, to, from_, status_callback): call.status = IVRCall.WIRED call.save() - for event in self.calls.events: + for event in self.events: ChannelLog.log_ivr_interaction(call, "Started call", event) except TwilioRestException as twilio_error: @@ -193,10 +193,10 @@ def download_media(self, media_url): return None # pragma: needs cover def hangup(self, call): - response = self.calls.hangup(call.external_id) - for event in self.calls.events: + twilio_call = self.calls.get(call.external_id).update(status="completed") + for event in self.events: ChannelLog.log_ivr_interaction(call, "Hung up call", event) - return response + return twilio_call class VerboiceClient: # pragma: needs cover diff --git a/temba/ivr/tests.py b/temba/ivr/tests.py index f27e04b8959..2d1e6c0ead5 100644 --- a/temba/ivr/tests.py +++ b/temba/ivr/tests.py @@ -49,7 +49,7 @@ def tearDown(self): @patch("nexmo.Client.create_call") @patch("nexmo.Client.update_call") @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_preferred_channel(self, mock_update_call, mock_create_call, mock_create_application): mock_create_application.return_value = dict(id="app-id", keys=dict(private_key="private-key\n")) mock_create_call.return_value = dict(uuid="12345") @@ -107,10 +107,10 @@ def test_preferred_channel(self, mock_update_call, mock_create_call, mock_create self.assertEqual("NX", call.channel.channel_type) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_twilio_failed_auth(self): def create(self, to=None, from_=None, url=None, status_callback=None): - from twilio import TwilioRestException + from twilio.base.exceptions import TwilioRestException raise TwilioRestException(403, "http://twilio.com", code=20003) @@ -137,13 +137,14 @@ def create(self, to=None, from_=None, url=None, status_callback=None): log.text, "Call ended. Could not authenticate with your Twilio account. " "Check your token and try again." ) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) + @patch("twilio.rest.api.v2010.account.call.CallInstance", MockTwilioClient.MockCallInstance) def test_call_logging(self): # create our ivr setup self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin) self.org.save() - with patch("twilio.rest.resources.base.make_request") as mock: + with patch("twilio.rest.Client.request") as mock: mock.return_value = MockResponse(200, '{"sid": "CAa346467ca321c71dbd5e12f627deb854"}') self.import_file("capture_recording") flow = Flow.objects.filter(name="Capture Recording").first() @@ -166,13 +167,13 @@ def test_call_logging(self): self.assertEqual(log.response, mock.return_value.text) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_disable_calls_twilio(self): with self.settings(SEND_CALLS=False): self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin) self.org.save() - with patch("twilio.rest.resources.calls.Calls.create") as mock: + with patch("twilio.rest.api.v2010.account.call.CallList.create") as mock: self.import_file("call_me_maybe") flow = Flow.objects.filter(name="Call me maybe").first() @@ -212,7 +213,7 @@ def test_disable_calls_nexmo(self, mock_create_call, mock_create_application): self.assertEqual(IVRCall.FAILED, call.status) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_bogus_call(self): # create our ivr setup self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin) @@ -236,7 +237,7 @@ def test_bogus_call(self): self.assertEqual(200, response.status_code) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_ivr_recording(self): # create our ivr setup @@ -543,7 +544,7 @@ def test_ivr_recording_with_nexmo(self, mock_create_call, mock_create_applicatio self.assertContains(response, "Kab00m!") @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_ivr_subflow(self): with patch("temba.ivr.models.IVRCall.start_call") as start_call: @@ -611,7 +612,7 @@ def test_ivr_subflow(self): self.assertFalse(run.is_completed()) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_ivr_start_flow(self): self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin) self.org.save() @@ -641,7 +642,7 @@ def test_ivr_start_flow(self): self.assertTrue(Msg.objects.filter(direction=IVRCall.OUTGOING, contact=ben, text="You said foo!").first()) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_ivr_call_redirect(self): self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin) self.org.save() @@ -674,7 +675,7 @@ def test_ivr_call_redirect(self): self.assertEqual(IVRCall.COMPLETED, call.status) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_text_trigger_ivr(self): self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin) self.org.save() @@ -706,7 +707,7 @@ def test_text_trigger_ivr(self): self.assertEqual(2, Msg.objects.all().count()) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_non_blocking_rule_ivr(self): self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin) @@ -757,7 +758,7 @@ def test_non_blocking_rule_ivr(self): self.assertEqual("Hi there Eminem", Msg.objects.filter(direction="O", contact=eminem).first().text) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_ivr_digit_gather(self): self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin) @@ -1001,7 +1002,7 @@ def test_ivr_subflow_with_nexmo(self, mock_create_call, mock_create_application) mock_create_call.assert_called_once() @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_ivr_simulation(self): # import an ivr flow @@ -1021,7 +1022,7 @@ def test_ivr_simulation(self): self.assertNotEqual(first_call.id, second_call.id) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_ivr_flow(self): # should be able to create an ivr flow @@ -1250,7 +1251,7 @@ def test_ivr_flow(self): self.assertEqual(timedelta(seconds=23), call.get_duration()) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_ivr_status_update(self): def test_status_update(call_to_update, twilio_status, temba_status, channel_type): call_to_update.ended_on = None @@ -1316,7 +1317,7 @@ def test_status_update(call_to_update, twilio_status, temba_status, channel_type self.assertRaises(ValueError, test_status_update, call, "busy", IVRCall.BUSY, "T") @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_rule_first_ivr_flow(self): # connect it and check our client is configured self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin) @@ -1355,7 +1356,7 @@ def test_rule_first_ivr_flow(self): self.assertFalse(Flow.find_and_handle(msg)[0]) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_incoming_call(self): # connect it and check our client is configured @@ -1428,7 +1429,7 @@ def test_incoming_call(self): self.assertContains(response, "No channel found", status_code=400) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_incoming_start(self): self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin) self.org.save() @@ -1713,7 +1714,7 @@ def test_no_flow_for_incoming_nexmo(self, mock_create_application): self.assertTrue(FlowRun.objects.filter(flow=flow)) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_no_channel_for_call(self): self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin) self.org.save() @@ -1734,7 +1735,7 @@ def test_no_channel_for_call(self): self.assertFalse(IVRCall.objects.all()) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_no_flow_for_incoming(self): self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin) self.org.save() @@ -1755,7 +1756,7 @@ def test_no_flow_for_incoming(self): self.assertTrue(FlowRun.objects.filter(flow=flow)) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_no_twilio_connected(self): # create an inbound call post_data = dict( @@ -1766,7 +1767,7 @@ def test_no_twilio_connected(self): self.assertEqual(response.status_code, 400) @patch("temba.ivr.clients.TwilioClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_download_media_twilio(self): self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin) self.org.save() diff --git a/temba/orgs/tests.py b/temba/orgs/tests.py index 8746d79d3be..fd366802cf1 100644 --- a/temba/orgs/tests.py +++ b/temba/orgs/tests.py @@ -1567,8 +1567,8 @@ def test_topups(self): self.assertTrue(self.org.is_multi_user_tier()) self.assertTrue(self.org.is_multi_org_tier()) - @patch("temba.orgs.views.TwilioRestClient", MockTwilioClient) - @patch("twilio.util.RequestValidator", MockRequestValidator) + @patch("temba.orgs.views.Client", MockTwilioClient) + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) def test_twilio_connect(self): with patch("temba.tests.twilio.MockTwilioClient.MockAccounts.get") as mock_get: mock_get.return_value = MockTwilioClient.MockAccount("Full") diff --git a/temba/orgs/views.py b/temba/orgs/views.py index c0601ce3b24..926a9ac26c1 100644 --- a/temba/orgs/views.py +++ b/temba/orgs/views.py @@ -20,7 +20,7 @@ SmartTemplateView, SmartUpdateView, ) -from twilio.rest import TwilioRestClient +from twilio.rest import Client from django import forms from django.conf import settings @@ -730,10 +730,10 @@ def clean(self): raise ValidationError(_("You must enter your Twilio Account Token")) try: - client = TwilioRestClient(account_sid, account_token) + client = Client(account_sid, account_token) # get the actual primary auth tokens from twilio and use them - account = client.accounts.get(account_sid) + account = client.api.accounts(account_sid) self.cleaned_data["account_sid"] = account.sid self.cleaned_data["account_token"] = account.auth_token except Exception: @@ -2463,10 +2463,10 @@ def clean(self): raise ValidationError(_("You must enter your Twilio Account Token")) try: - client = TwilioRestClient(account_sid, account_token) + client = Client(account_sid, account_token) # get the actual primary auth tokens from twilio and use them - account = client.accounts.get(account_sid) + account = client.api.accounts(account_sid) self.cleaned_data["account_sid"] = account.sid self.cleaned_data["account_token"] = account.auth_token except Exception: # pragma: needs cover diff --git a/temba/tests/twilio.py b/temba/tests/twilio.py index 3056b410b89..6fd228e6222 100644 --- a/temba/tests/twilio.py +++ b/temba/tests/twilio.py @@ -1,4 +1,4 @@ -from twilio.util import RequestValidator +from twilio.request_validator import RequestValidator from temba.ivr.clients import TwilioClient @@ -15,12 +15,28 @@ class MockTwilioClient(TwilioClient): def __init__(self, sid, token, org=None, base=None): self.org = org self.base = base - self.applications = MockTwilioClient.MockApplications() - self.calls = MockTwilioClient.MockCalls() - self.accounts = MockTwilioClient.MockAccounts() - self.phone_numbers = MockTwilioClient.MockPhoneNumbers() - self.sms = MockTwilioClient.MockSMS() self.auth = ["", "FakeRequestToken"] + self.events = [] + + @property + def applications(self): + return MockTwilioClient.MockApplications() + + @property + def calls(self): + return MockTwilioClient.MockCalls() + + @property + def accounts(self): + return MockTwilioClient.MockAccounts() + + @property + def phone_numbers(self): + return MockTwilioClient.MockPhoneNumbers() + + @property + def messages(self): + return MockTwilioClient.MockSMS() def validate(self, request): return True @@ -45,13 +61,13 @@ def __init__(self, *args): self.uri = "/SMS" self.short_codes = MockTwilioClient.MockShortCodes() - class MockCall(object): - def __init__(self, to=None, from_=None, url=None, status_callback=None): - self.to = to - self.from_ = from_ - self.url = url - self.status_callback = status_callback + class MockCallInstance(object): + def __init__(self, *args, **kwargs): self.sid = "CallSid" + pass + + def update(self, status): + print("Updating call %s to status %s" % (self.sid, status)) class MockApplication(object): def __init__(self, friendly_name): @@ -106,8 +122,11 @@ class MockCalls(object): def __init__(self): self.events = [] + def get(self, *args): + return MockTwilioClient.MockCallInstance() + def create(self, to=None, from_=None, url=None, status_callback=None): - return MockTwilioClient.MockCall(to=to, from_=from_, url=url, status_callback=status_callback) + return MockTwilioClient.MockCallInstance(to=to, from_=from_, url=url, status_callback=status_callback) def hangup(self, external_id): print("Hanging up %s on Twilio" % external_id) diff --git a/temba/utils/twilio.py b/temba/utils/twilio.py index 5ca34151b21..b439178c277 100644 --- a/temba/utils/twilio.py +++ b/temba/utils/twilio.py @@ -1,10 +1,7 @@ -import json from urllib.parse import urlencode -from twilio.rest import TwilioRestClient -from twilio.rest.resources import Calls, Messages, Resource, make_twilio_request -from twilio.rest.resources.util import UNSET_TIMEOUT +from twilio.rest import Client from django.utils.encoding import force_text @@ -20,20 +17,12 @@ def encode_atom(atom): # pragma: no cover raise ValueError("list elements should be an integer, " "binary, or string") -class LoggingResource(Resource): # pragma: no cover +class TembaTwilioRestClient(Client): # pragma: no cover def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.events = [] def request(self, method, uri, **kwargs): - """ - Send an HTTP request to the resource. - - :raises: a :exc:`~twilio.TwilioRestException` - """ - if "timeout" not in kwargs and self.timeout is not UNSET_TIMEOUT: - kwargs["timeout"] = self.timeout - data = kwargs.get("data") if data is not None: udata = {} @@ -47,34 +36,14 @@ def request(self, method, uri, **kwargs): raise ValueError("data should be an integer, " "binary, or string, or sequence ") data = urlencode(udata, doseq=True) + del kwargs["auth"] event = HttpEvent(method, uri, data) - self.events.append(event) - resp = make_twilio_request(method, uri, auth=self.auth, **kwargs) + if "/messages" in uri.lower() or "/calls" in uri.lower(): + self.events.append(event) + resp = super().request(method, uri, auth=self.auth, **kwargs) event.url = resp.url event.status_code = resp.status_code event.response_body = force_text(resp.content) - if method == "DELETE": - return resp, {} - else: - return resp, json.loads(resp.content) - - -class LoggingCalls(LoggingResource, Calls): # pragma: no cover - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - -class LoggingMessages(LoggingResource, Messages): # pragma: nocover - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - -class TembaTwilioRestClient(TwilioRestClient): # pragma: no cover - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # replace endpoints we want logging for - self.messages = LoggingMessages(self.account_uri, self.auth, self.timeout) - self.calls = LoggingCalls(self.account_uri, self.auth, self.timeout) + return resp From 1d3c415d8de0ddbf3244ee45d8922e0be9e0d4ea Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Fri, 24 Aug 2018 16:34:11 +0200 Subject: [PATCH 02/12] Adapt to twilio 6.x changes --- temba/channels/types/twilio/tests.py | 20 ++-- temba/channels/types/twilio/type.py | 8 +- temba/channels/types/twilio/views.py | 20 ++-- .../types/twilio_messaging_service/views.py | 2 +- temba/channels/views.py | 4 +- temba/ivr/clients.py | 4 +- temba/tests/twilio.py | 103 +++++++++++++----- 7 files changed, 103 insertions(+), 58 deletions(-) diff --git a/temba/channels/types/twilio/tests.py b/temba/channels/types/twilio/tests.py index feb3acdf375..04ef35eb372 100644 --- a/temba/channels/types/twilio/tests.py +++ b/temba/channels/types/twilio/tests.py @@ -62,7 +62,7 @@ def test_claim(self): self.assertIn("account_trial", response.context) self.assertTrue(response.context["account_trial"]) - with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.search") as mock_search: + with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.stream") as mock_search: search_url = reverse("channels.channel_search_numbers") # try making empty request @@ -95,10 +95,10 @@ def test_claim(self): response.json()["error"], "Sorry, no numbers found, please enter another pattern and try again." ) - with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.list") as mock_numbers: + with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.stream") as mock_numbers: mock_numbers.return_value = [MockTwilioClient.MockPhoneNumber("+12062345678")] - with patch("temba.tests.twilio.MockTwilioClient.MockShortCodes.list") as mock_short_codes: + with patch("temba.tests.twilio.MockTwilioClient.MockShortCodes.stream") as mock_short_codes: mock_short_codes.return_value = [] response = self.client.get(claim_twilio) @@ -121,10 +121,10 @@ def test_claim(self): self.assertTrue(channel_config[Channel.CONFIG_NUMBER_SID]) # voice only number - with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.list") as mock_numbers: + with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.stream") as mock_numbers: mock_numbers.return_value = [MockTwilioClient.MockPhoneNumber("+554139087835")] - with patch("temba.tests.twilio.MockTwilioClient.MockShortCodes.list") as mock_short_codes: + with patch("temba.tests.twilio.MockTwilioClient.MockShortCodes.stream") as mock_short_codes: mock_short_codes.return_value = [] Channel.objects.all().delete() @@ -139,10 +139,10 @@ def test_claim(self): channel = Channel.objects.get(channel_type="T", org=self.org) self.assertEqual(channel.role, Channel.ROLE_CALL + Channel.ROLE_ANSWER) - with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.list") as mock_numbers: + with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.stream") as mock_numbers: mock_numbers.return_value = [MockTwilioClient.MockPhoneNumber("+4545335500")] - with patch("temba.tests.twilio.MockTwilioClient.MockShortCodes.list") as mock_short_codes: + with patch("temba.tests.twilio.MockTwilioClient.MockShortCodes.stream") as mock_short_codes: mock_short_codes.return_value = [] Channel.objects.all().delete() @@ -158,10 +158,10 @@ def test_claim(self): # make sure it is actually connected Channel.objects.get(channel_type="T", org=self.org) - with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.list") as mock_numbers: + with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.stream") as mock_numbers: mock_numbers.return_value = [] - with patch("temba.tests.twilio.MockTwilioClient.MockShortCodes.list") as mock_short_codes: + with patch("temba.tests.twilio.MockTwilioClient.MockShortCodes.stream") as mock_short_codes: mock_short_codes.return_value = [MockTwilioClient.MockShortCode("8080")] Channel.objects.all().delete() @@ -187,7 +187,7 @@ def test_claim(self): self.assertEqual("T", twilio_channel.channel_type) with self.settings(IS_PROD=True): - with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.update") as mock_numbers: + with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumber.update") as mock_numbers: # our twilio channel removal should fail on bad auth mock_numbers.side_effect = TwilioRestException( 401, "http://twilio", msg="Authentication Failure", code=20003 diff --git a/temba/channels/types/twilio/type.py b/temba/channels/types/twilio/type.py index 2224fc04c40..3bd08a573dc 100644 --- a/temba/channels/types/twilio/type.py +++ b/temba/channels/types/twilio/type.py @@ -52,16 +52,16 @@ def deactivate(self, channel): try: try: number_sid = channel.bod or channel.config.get("number_sid") - client.phone_numbers.update(number_sid, **number_update_args) + client.api.incoming_phone_numbers.get(number_sid).update(**number_update_args) except Exception: if client: - matching = client.phone_numbers.list(phone_number=channel.address) + matching = client.api.incoming_phone_numbers.stream(phone_number=channel.address) if matching: - client.phone_numbers.update(matching[0].sid, **number_update_args) + client.api.incoming_phone_numbers.get(matching[0].sid).update(**number_update_args) if "application_sid" in config: try: - client.applications.delete(sid=config["application_sid"]) + client.api.applications.get(sid=config["application_sid"]).delete() except TwilioRestException: # pragma: no cover pass diff --git a/temba/channels/types/twilio/views.py b/temba/channels/types/twilio/views.py index f6d643a7935..9c85cbb6d82 100644 --- a/temba/channels/types/twilio/views.py +++ b/temba/channels/types/twilio/views.py @@ -54,7 +54,7 @@ def pre_process(self, *args, **kwargs): self.client = org.get_twilio_client() if not self.client: return HttpResponseRedirect(reverse("orgs.org_twilio_connect")) - self.account = self.client.accounts.get(org.config[ACCOUNT_SID]) + self.account = self.client.api.account.get(org.config[ACCOUNT_SID]).fetch() except TwilioRestException: return HttpResponseRedirect(reverse("orgs.org_twilio_connect")) @@ -78,8 +78,8 @@ def get_context_data(self, **kwargs): def get_existing_numbers(self, org): client = org.get_twilio_client() if client: - twilio_account_numbers = client.phone_numbers.list(page_size=1000) - twilio_short_codes = client.sms.short_codes.list(page_size=1000) + twilio_account_numbers = client.api.incoming_phone_numbers.stream(page_size=1000) + twilio_short_codes = client.api.short_codes.stream(page_size=1000) numbers = [] for number in twilio_account_numbers: @@ -107,7 +107,7 @@ def claim_number(self, user, phone_number, country, role): org = user.get_org() client = org.get_twilio_client() - twilio_phones = client.phone_numbers.list(phone_number=phone_number) + twilio_phones = client.api.incoming_phone_numbers.stream(phone_number=phone_number) channel_uuid = uuid4() # create new TwiML app @@ -118,7 +118,7 @@ def claim_number(self, user, phone_number, country, role): ) new_voice_url = "https://" + callback_domain + reverse("handlers.twilio_handler", args=["voice", channel_uuid]) - new_app = client.applications.create( + new_app = client.api.applications.create( friendly_name="%s/%s" % (callback_domain.lower(), channel_uuid), sms_url=new_receive_url, sms_method="POST", @@ -131,13 +131,13 @@ def claim_number(self, user, phone_number, country, role): is_short_code = len(phone_number) <= 6 if is_short_code: - short_codes = client.sms.short_codes.list(short_code=phone_number) + short_codes = client.api.short_codes.stream(short_code=phone_number) if short_codes: short_code = short_codes[0] number_sid = short_code.sid app_url = "https://" + callback_domain + "%s" % reverse("courier.t", args=[channel_uuid, "receive"]) - client.sms.short_codes.update(number_sid, sms_url=app_url, sms_method="POST") + client.api.short_codes.get(number_sid).update(sms_url=app_url, sms_method="POST") role = Channel.ROLE_SEND + Channel.ROLE_RECEIVE phone = phone_number @@ -152,12 +152,12 @@ def claim_number(self, user, phone_number, country, role): else: if twilio_phones: twilio_phone = twilio_phones[0] - client.phone_numbers.update( - twilio_phone.sid, voice_application_sid=new_app.sid, sms_application_sid=new_app.sid + client.api.incoming_phone_numbers.get(twilio_phone.sid).update( + voice_application_sid=new_app.sid, sms_application_sid=new_app.sid ) else: # pragma: needs cover - twilio_phone = client.phone_numbers.purchase( + twilio_phone = client.api.incoming_phone_numbers.create( phone_number=phone_number, voice_application_sid=new_app.sid, sms_application_sid=new_app.sid ) diff --git a/temba/channels/types/twilio_messaging_service/views.py b/temba/channels/types/twilio_messaging_service/views.py index 109d816e783..d86d842e50e 100644 --- a/temba/channels/types/twilio_messaging_service/views.py +++ b/temba/channels/types/twilio_messaging_service/views.py @@ -34,7 +34,7 @@ def pre_process(self, *args, **kwargs): self.client = org.get_twilio_client() if not self.client: return HttpResponseRedirect(reverse("orgs.org_twilio_connect")) - self.account = self.client.accounts.get(org.config[ACCOUNT_SID]) + self.account = self.client.api.account.get(org.config[ACCOUNT_SID]).fetch() except TwilioRestException: return HttpResponseRedirect(reverse("orgs.org_twilio_connect")) diff --git a/temba/channels/views.py b/temba/channels/views.py index dad3d5b5e1c..cb43572d4cd 100644 --- a/temba/channels/views.py +++ b/temba/channels/views.py @@ -1800,13 +1800,13 @@ def search_available_numbers(self, client, **kwargs): kwargs["type"] = "local" try: - available_numbers += client.phone_numbers.search(**kwargs) + available_numbers += client.api.available_phone_numbers.stream(**kwargs) except TwilioRestException: # pragma: no cover pass kwargs["type"] = "mobile" try: - available_numbers += client.phone_numbers.search(**kwargs) + available_numbers += client.api.available_phone_numbers.stream(**kwargs) except TwilioRestException: # pragma: no cover pass diff --git a/temba/ivr/clients.py b/temba/ivr/clients.py index cc731afe07b..517cd80c920 100644 --- a/temba/ivr/clients.py +++ b/temba/ivr/clients.py @@ -137,7 +137,7 @@ def start_call(self, call, to, from_, status_callback): params = dict(to=to, from_=call.channel.address, url=status_callback, status_callback=status_callback) try: - twilio_call = self.calls.create(**params) + twilio_call = self.api.calls.create(**params) call.external_id = str(twilio_call.sid) # the call was successfully sent to the IVR provider @@ -193,7 +193,7 @@ def download_media(self, media_url): return None # pragma: needs cover def hangup(self, call): - twilio_call = self.calls.get(call.external_id).update(status="completed") + twilio_call = self.api.calls.get(call.external_id).update(status="completed") for event in self.events: ChannelLog.log_ivr_interaction(call, "Hung up call", event) return twilio_call diff --git a/temba/tests/twilio.py b/temba/tests/twilio.py index 6fd228e6222..5fe16086530 100644 --- a/temba/tests/twilio.py +++ b/temba/tests/twilio.py @@ -19,12 +19,12 @@ def __init__(self, sid, token, org=None, base=None): self.events = [] @property - def applications(self): - return MockTwilioClient.MockApplications() + def api(self): + return MockTwilioClient.MockAPI() @property - def calls(self): - return MockTwilioClient.MockCalls() + def applications(self): + return MockTwilioClient.MockApplications() @property def accounts(self): @@ -34,34 +34,76 @@ def accounts(self): def phone_numbers(self): return MockTwilioClient.MockPhoneNumbers() - @property - def messages(self): - return MockTwilioClient.MockSMS() - def validate(self, request): return True - class MockShortCode(object): + class MockInstanceResource(object): + def __init__(self, *args, **kwargs): + pass + + def fetch(self): + return self + + def update(self, **kwargs): + return True + + def delete(self, **kwargs): + return True + + def get(self, sid): + return self.stream()[0] + + class MockAPI(object): + def __init__(self, *args, **kwargs): + pass + + @property + def account(self): + return MockTwilioClient.MockAccounts().get("Full") + + @property + def incoming_phone_numbers(self): + return MockTwilioClient.MockPhoneNumbers() + + @property + def available_phone_numbers(self): + return MockTwilioClient.MockPhoneNumbers() + + @property + def short_codes(self): + return MockTwilioClient.MockShortCodes() + + @property + def applications(self): + return MockTwilioClient.MockApplications() + + @property + def calls(self): + return MockTwilioClient.MockCalls() + + @property + def messages(self): + return MockTwilioClient.MockInstanceResource() + + class MockShortCode(MockInstanceResource): def __init__(self, short_code): self.short_code = short_code self.sid = "ShortSid" - class MockShortCodes(object): + class MockShortCodes(MockInstanceResource): def __init__(self, *args): pass def list(self, short_code=None): return [MockTwilioClient.MockShortCode(short_code)] + def stream(self, *args, **kwargs): + return [MockTwilioClient.MockShortCode("1122")] + def update(self, sid, **kwargs): print("Updating short code with sid %s" % sid) - class MockSMS(object): - def __init__(self, *args): - self.uri = "/SMS" - self.short_codes = MockTwilioClient.MockShortCodes() - - class MockCallInstance(object): + class MockCallInstance(MockInstanceResource): def __init__(self, *args, **kwargs): self.sid = "CallSid" pass @@ -69,43 +111,46 @@ def __init__(self, *args, **kwargs): def update(self, status): print("Updating call %s to status %s" % (self.sid, status)) - class MockApplication(object): + class MockApplication(MockInstanceResource): def __init__(self, friendly_name): self.friendly_name = friendly_name self.sid = "TwilioTestSid" - class MockPhoneNumber(object): + class MockPhoneNumber(MockInstanceResource): def __init__(self, phone_number): self.phone_number = phone_number self.sid = "PhoneNumberSid" - class MockAccount(object): + class MockAccount(MockInstanceResource): def __init__(self, account_type, auth_token="AccountToken"): self.type = account_type self.auth_token = auth_token self.sid = "AccountSid" - class MockAccounts(object): + def get(self, sid): + return self + + class MockAccounts(MockInstanceResource): def __init__(self, *args): pass def get(self, account_type): return MockTwilioClient.MockAccount(account_type) - class MockPhoneNumbers(object): + class MockPhoneNumbers(MockInstanceResource): def __init__(self, *args): pass def list(self, phone_number=None): return [MockTwilioClient.MockPhoneNumber(phone_number)] + def stream(self, *args, **kwargs): + return [MockTwilioClient.MockPhoneNumber("+12062345678")] + def search(self, **kwargs): return [] - def update(self, sid, **kwargs): - print("Updating phone number with sid %s" % sid) - - class MockApplications(object): + class MockApplications(MockInstanceResource): def __init__(self, *args): pass @@ -115,12 +160,12 @@ def create(self, **kwargs): def list(self, friendly_name=None): return [MockTwilioClient.MockApplication(friendly_name)] - def delete(self, **kwargs): - return True + def get(self, sid): + return self.list()[0] - class MockCalls(object): + class MockCalls(MockInstanceResource): def __init__(self): - self.events = [] + pass def get(self, *args): return MockTwilioClient.MockCallInstance() From b08c855a0745e8471b62eba2f8439f15be1a6602 Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Fri, 24 Aug 2018 19:35:41 +0200 Subject: [PATCH 03/12] Fix Twilio account fetch to test creadentials validity --- temba/channels/types/twilio/views.py | 2 +- .../types/twilio_messaging_service/views.py | 2 +- temba/orgs/tests.py | 4 ++-- temba/orgs/views.py | 2 +- temba/tests/twilio.py | 15 +++------------ 5 files changed, 8 insertions(+), 17 deletions(-) diff --git a/temba/channels/types/twilio/views.py b/temba/channels/types/twilio/views.py index 9c85cbb6d82..b26c651b8e5 100644 --- a/temba/channels/types/twilio/views.py +++ b/temba/channels/types/twilio/views.py @@ -54,7 +54,7 @@ def pre_process(self, *args, **kwargs): self.client = org.get_twilio_client() if not self.client: return HttpResponseRedirect(reverse("orgs.org_twilio_connect")) - self.account = self.client.api.account.get(org.config[ACCOUNT_SID]).fetch() + self.account = self.client.api.account.fetch() except TwilioRestException: return HttpResponseRedirect(reverse("orgs.org_twilio_connect")) diff --git a/temba/channels/types/twilio_messaging_service/views.py b/temba/channels/types/twilio_messaging_service/views.py index d86d842e50e..0567d36a001 100644 --- a/temba/channels/types/twilio_messaging_service/views.py +++ b/temba/channels/types/twilio_messaging_service/views.py @@ -34,7 +34,7 @@ def pre_process(self, *args, **kwargs): self.client = org.get_twilio_client() if not self.client: return HttpResponseRedirect(reverse("orgs.org_twilio_connect")) - self.account = self.client.api.account.get(org.config[ACCOUNT_SID]).fetch() + self.account = self.client.api.account.fetch() except TwilioRestException: return HttpResponseRedirect(reverse("orgs.org_twilio_connect")) diff --git a/temba/orgs/tests.py b/temba/orgs/tests.py index fd366802cf1..d640e1c15eb 100644 --- a/temba/orgs/tests.py +++ b/temba/orgs/tests.py @@ -1609,9 +1609,9 @@ def test_twilio_connect(self): # when the user submit the secondary token, we use it to get the primary one from the rest API with patch("temba.tests.twilio.MockTwilioClient.MockAccounts.get") as mock_get_primary: - with patch("twilio.rest.resources.ListResource.get") as mock_list_resource_get: + with patch("twilio.rest.api.v2010.account.AccountContext.fetch") as mock_account_fetch: mock_get_primary.return_value = MockTwilioClient.MockAccount("Full", "PrimaryAccountToken") - mock_list_resource_get.return_value = MockTwilioClient.MockAccount("Full", "PrimaryAccountToken") + mock_account_fetch.return_value = MockTwilioClient.MockAccount("Full", "PrimaryAccountToken") response = self.client.post(connect_url, post_data) self.assertEqual(response.status_code, 302) diff --git a/temba/orgs/views.py b/temba/orgs/views.py index 926a9ac26c1..0fcf4ded485 100644 --- a/temba/orgs/views.py +++ b/temba/orgs/views.py @@ -733,7 +733,7 @@ def clean(self): client = Client(account_sid, account_token) # get the actual primary auth tokens from twilio and use them - account = client.api.accounts(account_sid) + account = client.api.account.fetch() self.cleaned_data["account_sid"] = account.sid self.cleaned_data["account_token"] = account.auth_token except Exception: diff --git a/temba/tests/twilio.py b/temba/tests/twilio.py index 5fe16086530..2d59d4f27b4 100644 --- a/temba/tests/twilio.py +++ b/temba/tests/twilio.py @@ -22,18 +22,6 @@ def __init__(self, sid, token, org=None, base=None): def api(self): return MockTwilioClient.MockAPI() - @property - def applications(self): - return MockTwilioClient.MockApplications() - - @property - def accounts(self): - return MockTwilioClient.MockAccounts() - - @property - def phone_numbers(self): - return MockTwilioClient.MockPhoneNumbers() - def validate(self, request): return True @@ -130,6 +118,9 @@ def __init__(self, account_type, auth_token="AccountToken"): def get(self, sid): return self + def fetch(self): + return self + class MockAccounts(MockInstanceResource): def __init__(self, *args): pass From a4cfa8d91cfecbfa682d223c932978552b46955b Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Mon, 27 Aug 2018 15:30:11 +0200 Subject: [PATCH 04/12] Fix getting account on Twilio Account view --- temba/orgs/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temba/orgs/views.py b/temba/orgs/views.py index 0fcf4ded485..533ed1ac2d9 100644 --- a/temba/orgs/views.py +++ b/temba/orgs/views.py @@ -2466,7 +2466,7 @@ def clean(self): client = Client(account_sid, account_token) # get the actual primary auth tokens from twilio and use them - account = client.api.accounts(account_sid) + account = client.api.account.fetch() self.cleaned_data["account_sid"] = account.sid self.cleaned_data["account_token"] = account.auth_token except Exception: # pragma: needs cover From 656d1b9fa28453ae03b08de156584bf685bbbddc Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Mon, 27 Aug 2018 16:32:22 +0200 Subject: [PATCH 05/12] Fix event url to use the request URI, no url attribute on response --- temba/utils/twilio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temba/utils/twilio.py b/temba/utils/twilio.py index b439178c277..6295b79785a 100644 --- a/temba/utils/twilio.py +++ b/temba/utils/twilio.py @@ -42,7 +42,7 @@ def request(self, method, uri, **kwargs): self.events.append(event) resp = super().request(method, uri, auth=self.auth, **kwargs) - event.url = resp.url + event.url = uri event.status_code = resp.status_code event.response_body = force_text(resp.content) From fc0aadf06beded52c811ca9153a12f972564c38b Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Mon, 27 Aug 2018 18:33:04 +0200 Subject: [PATCH 06/12] Use list() on iterator from Twilio --- temba/channels/types/twilio/type.py | 2 +- temba/channels/types/twilio/views.py | 4 ++-- temba/tests/twilio.py | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/temba/channels/types/twilio/type.py b/temba/channels/types/twilio/type.py index 3bd08a573dc..c342c540d6b 100644 --- a/temba/channels/types/twilio/type.py +++ b/temba/channels/types/twilio/type.py @@ -55,7 +55,7 @@ def deactivate(self, channel): client.api.incoming_phone_numbers.get(number_sid).update(**number_update_args) except Exception: if client: - matching = client.api.incoming_phone_numbers.stream(phone_number=channel.address) + matching = list(client.api.incoming_phone_numbers.stream(phone_number=channel.address)) if matching: client.api.incoming_phone_numbers.get(matching[0].sid).update(**number_update_args) diff --git a/temba/channels/types/twilio/views.py b/temba/channels/types/twilio/views.py index b26c651b8e5..9816f9fb382 100644 --- a/temba/channels/types/twilio/views.py +++ b/temba/channels/types/twilio/views.py @@ -107,7 +107,7 @@ def claim_number(self, user, phone_number, country, role): org = user.get_org() client = org.get_twilio_client() - twilio_phones = client.api.incoming_phone_numbers.stream(phone_number=phone_number) + twilio_phones = list(client.api.incoming_phone_numbers.stream(phone_number=phone_number)) channel_uuid = uuid4() # create new TwiML app @@ -131,7 +131,7 @@ def claim_number(self, user, phone_number, country, role): is_short_code = len(phone_number) <= 6 if is_short_code: - short_codes = client.api.short_codes.stream(short_code=phone_number) + short_codes = list(client.api.short_codes.stream(short_code=phone_number)) if short_codes: short_code = short_codes[0] diff --git a/temba/tests/twilio.py b/temba/tests/twilio.py index 2d59d4f27b4..2f992b28426 100644 --- a/temba/tests/twilio.py +++ b/temba/tests/twilio.py @@ -39,7 +39,7 @@ def delete(self, **kwargs): return True def get(self, sid): - return self.stream()[0] + return list(self.stream())[0] class MockAPI(object): def __init__(self, *args, **kwargs): @@ -141,6 +141,10 @@ def stream(self, *args, **kwargs): def search(self, **kwargs): return [] + def create(self, *args, **kwargs): + phone_number = kwargs["phone_number"] + return MockTwilioClient.MockPhoneNumber(phone_number) + class MockApplications(MockInstanceResource): def __init__(self, *args): pass From ec20bb5a1a2a29e2333c13cffd2d92d08f85216b Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Tue, 28 Aug 2018 13:48:26 +0200 Subject: [PATCH 07/12] Use snake case for parameters to conform to twilio 6.x --- temba/flows/models.py | 4 ++-- temba/utils/nexmo.py | 10 +++++----- temba/utils/tests.py | 12 ++++++------ temba/utils/voicexml.py | 10 +++++----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/temba/flows/models.py b/temba/flows/models.py index 052e592255c..fc00919f0ab 100644 --- a/temba/flows/models.py +++ b/temba/flows/models.py @@ -4613,10 +4613,10 @@ def get_voice_input(self, voice_response, action=None): if self.ruleset_type in [RuleSet.TYPE_WAIT_RECORDING, RuleSet.TYPE_SUBFLOW]: return voice_response elif self.ruleset_type == RuleSet.TYPE_WAIT_DIGITS: - return voice_response.gather(finishOnKey=self.finished_key, timeout=120, action=action) + return voice_response.gather(finish_on_key=self.finished_key, timeout=120, action=action) else: # otherwise we assume it's single digit entry - return voice_response.gather(numDigits=1, timeout=120, action=action) + return voice_response.gather(num_digits=1, timeout=120, action=action) def is_pause(self): return self.ruleset_type in RuleSet.TYPE_WAIT diff --git a/temba/utils/nexmo.py b/temba/utils/nexmo.py index 3b6d1f97009..f28967e49d4 100644 --- a/temba/utils/nexmo.py +++ b/temba/utils/nexmo.py @@ -235,10 +235,10 @@ def gather(self, **kwargs): result["eventMethod"] = method result["eventUrl"] = [kwargs.get("action")] - result["submitOnHash"] = kwargs.get("finishOnKey", "#") == "#" + result["submitOnHash"] = kwargs.get("finish_on_key", "#") == "#" - if kwargs.get("numDigits", False): - result["maxDigits"] = int(str(kwargs.get("numDigits"))) + if kwargs.get("num_digits", False): + result["maxDigits"] = int(str(kwargs.get("num_digits"))) if kwargs.get("timeout", False): result["timeOut"] = int(str(kwargs.get("timeout"))) @@ -249,8 +249,8 @@ def gather(self, **kwargs): def record(self, **kwargs): result = dict(format="wav", endOnSilence=4, endOnKey="#", beepStart=True, action="record") - if kwargs.get("maxLength", False): - result["timeOut"] = int(str(kwargs.get("maxLength"))) + if kwargs.get("max_length", False): + result["timeOut"] = int(str(kwargs.get("max_length"))) if kwargs.get("action", False): method = kwargs.get("method", "post") diff --git a/temba/utils/tests.py b/temba/utils/tests.py index cbc78e09b22..7fc0f2161bc 100644 --- a/temba/utils/tests.py +++ b/temba/utils/tests.py @@ -1466,7 +1466,7 @@ def test_gather(self): ) response = voicexml.VXMLResponse() - response.gather(action="http://example.com", numDigits=1, timeout=45, finishOnKey="*") + response.gather(action="http://example.com", num_digits=1, timeout=45, finish_on_key="*") self.assertEqual( str(response), @@ -1489,7 +1489,7 @@ def test_record(self): ) response = voicexml.VXMLResponse() - response.record(action="http://example.com", method="post", maxLength=60) + response.record(action="http://example.com", method="post", max_length=60) self.assertEqual( str(response), @@ -1584,7 +1584,7 @@ def test_bargeIn(self): response.say("Hello") response.redirect("http://example.com/") response.say("Please make a recording") - response.record(action="http://example.com", method="post", maxLength=60) + response.record(action="http://example.com", method="post", max_length=60) response.say("Thanks") response.say("Allo") response.say("Cool") @@ -1674,7 +1674,7 @@ def test_gather(self): ) response = NCCOResponse() - response.gather(action="http://example.com", numDigits=1, timeout=45, finishOnKey="*") + response.gather(action="http://example.com", num_digits=1, timeout=45, finish_on_key="*") self.assertEqual( json.loads(str(response)), @@ -1703,7 +1703,7 @@ def test_record(self): ) response = NCCOResponse() - response.record(action="http://example.com", method="post", maxLength=60) + response.record(action="http://example.com", method="post", max_length=60) self.assertEqual( json.loads(str(response)), @@ -1722,7 +1722,7 @@ def test_record(self): ], ) response = NCCOResponse() - response.record(action="http://example.com?param=12", method="post", maxLength=60) + response.record(action="http://example.com?param=12", method="post", max_length=60) self.assertEqual( json.loads(str(response)), diff --git a/temba/utils/voicexml.py b/temba/utils/voicexml.py index 68ed15c1348..0e65caa810a 100644 --- a/temba/utils/voicexml.py +++ b/temba/utils/voicexml.py @@ -74,12 +74,12 @@ def gather(self, **kwargs): result += 'termtimeout="' + str(kwargs.get("timeout")) + 's" ' result += 'timeout="' + str(kwargs.get("timeout")) + 's" ' - result += 'termchar="%s" ' % kwargs.get("finishOnKey", "#") + result += 'termchar="%s" ' % kwargs.get("finish_on_key", "#") result += 'src="builtin:dtmf/digits' - if kwargs.get("numDigits", False): - result += "?minlength=%s;maxlength=%s" % (kwargs.get("numDigits"), kwargs.get("numDigits")) + if kwargs.get("num_digits", False): + result += "?minlength=%s;maxlength=%s" % (kwargs.get("num_digits"), kwargs.get("num_digits")) result += '" />' @@ -99,8 +99,8 @@ def gather(self, **kwargs): def record(self, **kwargs): result = ' Date: Wed, 29 Aug 2018 12:49:57 +0200 Subject: [PATCH 08/12] Test mock to return generator for stream method --- temba/channels/types/twilio/tests.py | 60 +++++++++++++++------------- temba/channels/types/twilio/views.py | 13 +++--- temba/tests/twilio.py | 8 ++-- 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/temba/channels/types/twilio/tests.py b/temba/channels/types/twilio/tests.py index 04ef35eb372..94acb608c0f 100644 --- a/temba/channels/types/twilio/tests.py +++ b/temba/channels/types/twilio/tests.py @@ -70,36 +70,37 @@ def test_claim(self): self.assertEqual(response.json(), []) # try searching for US number - mock_search.return_value = [MockTwilioClient.MockPhoneNumber("+12062345678")] + mock_search.return_value = iter([MockTwilioClient.MockPhoneNumber("+12062345678")]) response = self.client.post(search_url, {"country": "US", "area_code": "206"}) - self.assertEqual(response.json(), ["+1 206-234-5678", "+1 206-234-5678"]) + self.assertEqual(response.json(), ["+1 206-234-5678"]) # try searching without area code + mock_search.return_value = iter([MockTwilioClient.MockPhoneNumber("+12062345678")]) response = self.client.post(search_url, {"country": "US", "area_code": ""}) - self.assertEqual(response.json(), ["+1 206-234-5678", "+1 206-234-5678"]) + self.assertEqual(response.json(), ["+1 206-234-5678"]) - mock_search.return_value = [] + mock_search.return_value = iter([]) response = self.client.post(search_url, {"country": "US", "area_code": ""}) self.assertEqual( response.json()["error"], "Sorry, no numbers found, please enter another area code and try again." ) # try searching for non-US number - mock_search.return_value = [MockTwilioClient.MockPhoneNumber("+442812345678")] + mock_search.return_value = iter([MockTwilioClient.MockPhoneNumber("+442812345678")]) response = self.client.post(search_url, {"country": "GB", "area_code": "028"}) - self.assertEqual(response.json(), ["+44 28 1234 5678", "+44 28 1234 5678"]) + self.assertEqual(response.json(), ["+44 28 1234 5678"]) - mock_search.return_value = [] + mock_search.return_value = iter([]) response = self.client.post(search_url, {"country": "GB", "area_code": ""}) self.assertEqual( response.json()["error"], "Sorry, no numbers found, please enter another pattern and try again." ) with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.stream") as mock_numbers: - mock_numbers.return_value = [MockTwilioClient.MockPhoneNumber("+12062345678")] + mock_numbers.return_value = iter([MockTwilioClient.MockPhoneNumber("+12062345678")]) with patch("temba.tests.twilio.MockTwilioClient.MockShortCodes.stream") as mock_short_codes: - mock_short_codes.return_value = [] + mock_short_codes.return_value = iter([]) response = self.client.get(claim_twilio) self.assertContains(response, "206-234-5678") @@ -122,10 +123,10 @@ def test_claim(self): # voice only number with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.stream") as mock_numbers: - mock_numbers.return_value = [MockTwilioClient.MockPhoneNumber("+554139087835")] + mock_numbers.return_value = iter([MockTwilioClient.MockPhoneNumber("+554139087835")]) with patch("temba.tests.twilio.MockTwilioClient.MockShortCodes.stream") as mock_short_codes: - mock_short_codes.return_value = [] + mock_short_codes.return_value = iter([]) Channel.objects.all().delete() response = self.client.get(claim_twilio) @@ -140,10 +141,10 @@ def test_claim(self): self.assertEqual(channel.role, Channel.ROLE_CALL + Channel.ROLE_ANSWER) with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.stream") as mock_numbers: - mock_numbers.return_value = [MockTwilioClient.MockPhoneNumber("+4545335500")] + mock_numbers.return_value = iter([MockTwilioClient.MockPhoneNumber("+4545335500")]) with patch("temba.tests.twilio.MockTwilioClient.MockShortCodes.stream") as mock_short_codes: - mock_short_codes.return_value = [] + mock_short_codes.return_value = iter([]) Channel.objects.all().delete() @@ -159,26 +160,31 @@ def test_claim(self): Channel.objects.get(channel_type="T", org=self.org) with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.stream") as mock_numbers: - mock_numbers.return_value = [] + mock_numbers.return_value = iter([]) with patch("temba.tests.twilio.MockTwilioClient.MockShortCodes.stream") as mock_short_codes: - mock_short_codes.return_value = [MockTwilioClient.MockShortCode("8080")] - Channel.objects.all().delete() + mock_short_codes.return_value = iter([MockTwilioClient.MockShortCode("8080")]) - self.org.timezone = "America/New_York" - self.org.save() + with patch("temba.tests.twilio.MockTwilioClient.MockShortCodes.get") as mock_short_codes_get: + mock_short_codes_get.return_value = MockTwilioClient.MockShortCode("8080") - response = self.client.get(claim_twilio) - self.assertContains(response, "8080") - self.assertContains(response, 'class="country">US') # we look up the country from the timezone + Channel.objects.all().delete() - # claim it - response = self.client.post(claim_twilio, dict(country="US", phone_number="8080")) - self.assertRedirects(response, reverse("public.public_welcome") + "?success") - self.assertEqual(mock_numbers.call_args_list[0][1], {"page_size": 1000}) + self.org.timezone = "America/New_York" + self.org.save() - # make sure it is actually connected - Channel.objects.get(channel_type="T", org=self.org) + response = self.client.get(claim_twilio) + self.assertContains(response, "8080") + self.assertContains(response, 'class="country">US') # we look up the country from the timezone + + # claim it + mock_short_codes.return_value = iter([MockTwilioClient.MockShortCode("8080")]) + response = self.client.post(claim_twilio, dict(country="US", phone_number="8080")) + self.assertRedirects(response, reverse("public.public_welcome") + "?success") + self.assertEqual(mock_numbers.call_args_list[0][1], {"page_size": 1000}) + + # make sure it is actually connected + Channel.objects.get(channel_type="T", org=self.org) twilio_channel = self.org.channels.all().first() # make channel support both sms and voice to check we clear both applications diff --git a/temba/channels/types/twilio/views.py b/temba/channels/types/twilio/views.py index 9816f9fb382..fec6a54d0a0 100644 --- a/temba/channels/types/twilio/views.py +++ b/temba/channels/types/twilio/views.py @@ -107,7 +107,7 @@ def claim_number(self, user, phone_number, country, role): org = user.get_org() client = org.get_twilio_client() - twilio_phones = list(client.api.incoming_phone_numbers.stream(phone_number=phone_number)) + twilio_phones = client.api.incoming_phone_numbers.stream(phone_number=phone_number) channel_uuid = uuid4() # create new TwiML app @@ -131,10 +131,10 @@ def claim_number(self, user, phone_number, country, role): is_short_code = len(phone_number) <= 6 if is_short_code: - short_codes = list(client.api.short_codes.stream(short_code=phone_number)) + short_codes = client.api.short_codes.stream(short_code=phone_number) + short_code = next(short_codes, None) - if short_codes: - short_code = short_codes[0] + if short_code: number_sid = short_code.sid app_url = "https://" + callback_domain + "%s" % reverse("courier.t", args=[channel_uuid, "receive"]) client.api.short_codes.get(number_sid).update(sms_url=app_url, sms_method="POST") @@ -150,8 +150,9 @@ def claim_number(self, user, phone_number, country, role): ) ) else: - if twilio_phones: - twilio_phone = twilio_phones[0] + twilio_phone = next(twilio_phones, None) + if twilio_phone: + client.api.incoming_phone_numbers.get(twilio_phone.sid).update( voice_application_sid=new_app.sid, sms_application_sid=new_app.sid ) diff --git a/temba/tests/twilio.py b/temba/tests/twilio.py index 2f992b28426..e08a83272e0 100644 --- a/temba/tests/twilio.py +++ b/temba/tests/twilio.py @@ -39,7 +39,9 @@ def delete(self, **kwargs): return True def get(self, sid): - return list(self.stream())[0] + objs = list(self.stream()) + if len(objs) > 0: + return objs[0] class MockAPI(object): def __init__(self, *args, **kwargs): @@ -86,7 +88,7 @@ def list(self, short_code=None): return [MockTwilioClient.MockShortCode(short_code)] def stream(self, *args, **kwargs): - return [MockTwilioClient.MockShortCode("1122")] + return iter([MockTwilioClient.MockShortCode("1122")]) def update(self, sid, **kwargs): print("Updating short code with sid %s" % sid) @@ -136,7 +138,7 @@ def list(self, phone_number=None): return [MockTwilioClient.MockPhoneNumber(phone_number)] def stream(self, *args, **kwargs): - return [MockTwilioClient.MockPhoneNumber("+12062345678")] + return iter([MockTwilioClient.MockPhoneNumber("+12062345678")]) def search(self, **kwargs): return [] From 4596b9b13d975b15cd9e3f378d9cf8bf7bc2d512 Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Wed, 29 Aug 2018 13:15:54 +0200 Subject: [PATCH 09/12] Proper use of generator --- temba/channels/types/twilio/type.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/temba/channels/types/twilio/type.py b/temba/channels/types/twilio/type.py index c342c540d6b..81ebbcb203d 100644 --- a/temba/channels/types/twilio/type.py +++ b/temba/channels/types/twilio/type.py @@ -55,9 +55,10 @@ def deactivate(self, channel): client.api.incoming_phone_numbers.get(number_sid).update(**number_update_args) except Exception: if client: - matching = list(client.api.incoming_phone_numbers.stream(phone_number=channel.address)) - if matching: - client.api.incoming_phone_numbers.get(matching[0].sid).update(**number_update_args) + matching = client.api.incoming_phone_numbers.stream(phone_number=channel.address) + first_match = next(matching, None) + if first_match: + client.api.incoming_phone_numbers.get(first_match.sid).update(**number_update_args) if "application_sid" in config: try: From 7694ee6912ce8315a893529a096dfab75dd899f7 Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Wed, 29 Aug 2018 13:27:33 +0200 Subject: [PATCH 10/12] Use list method for searching Twilio numbers --- temba/channels/types/twilio/tests.py | 17 ++++++++--------- temba/channels/views.py | 4 ++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/temba/channels/types/twilio/tests.py b/temba/channels/types/twilio/tests.py index 94acb608c0f..c2035ce77b2 100644 --- a/temba/channels/types/twilio/tests.py +++ b/temba/channels/types/twilio/tests.py @@ -62,7 +62,7 @@ def test_claim(self): self.assertIn("account_trial", response.context) self.assertTrue(response.context["account_trial"]) - with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.stream") as mock_search: + with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.list") as mock_search: search_url = reverse("channels.channel_search_numbers") # try making empty request @@ -70,27 +70,26 @@ def test_claim(self): self.assertEqual(response.json(), []) # try searching for US number - mock_search.return_value = iter([MockTwilioClient.MockPhoneNumber("+12062345678")]) + mock_search.return_value = [MockTwilioClient.MockPhoneNumber("+12062345678")] response = self.client.post(search_url, {"country": "US", "area_code": "206"}) - self.assertEqual(response.json(), ["+1 206-234-5678"]) + self.assertEqual(response.json(), ["+1 206-234-5678", "+1 206-234-5678"]) # try searching without area code - mock_search.return_value = iter([MockTwilioClient.MockPhoneNumber("+12062345678")]) response = self.client.post(search_url, {"country": "US", "area_code": ""}) - self.assertEqual(response.json(), ["+1 206-234-5678"]) + self.assertEqual(response.json(), ["+1 206-234-5678", "+1 206-234-5678"]) - mock_search.return_value = iter([]) + mock_search.return_value = [] response = self.client.post(search_url, {"country": "US", "area_code": ""}) self.assertEqual( response.json()["error"], "Sorry, no numbers found, please enter another area code and try again." ) # try searching for non-US number - mock_search.return_value = iter([MockTwilioClient.MockPhoneNumber("+442812345678")]) + mock_search.return_value = [MockTwilioClient.MockPhoneNumber("+442812345678")] response = self.client.post(search_url, {"country": "GB", "area_code": "028"}) - self.assertEqual(response.json(), ["+44 28 1234 5678"]) + self.assertEqual(response.json(), ["+44 28 1234 5678", "+44 28 1234 5678"]) - mock_search.return_value = iter([]) + mock_search.return_value = [] response = self.client.post(search_url, {"country": "GB", "area_code": ""}) self.assertEqual( response.json()["error"], "Sorry, no numbers found, please enter another pattern and try again." diff --git a/temba/channels/views.py b/temba/channels/views.py index cb43572d4cd..97f97efb4fa 100644 --- a/temba/channels/views.py +++ b/temba/channels/views.py @@ -1800,13 +1800,13 @@ def search_available_numbers(self, client, **kwargs): kwargs["type"] = "local" try: - available_numbers += client.api.available_phone_numbers.stream(**kwargs) + available_numbers += client.api.available_phone_numbers.list(**kwargs) except TwilioRestException: # pragma: no cover pass kwargs["type"] = "mobile" try: - available_numbers += client.api.available_phone_numbers.stream(**kwargs) + available_numbers += client.api.available_phone_numbers.list(**kwargs) except TwilioRestException: # pragma: no cover pass From 0f5114d9a4fc29774ef88eee1e45fc85cb735d8e Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Wed, 29 Aug 2018 17:13:56 +0200 Subject: [PATCH 11/12] Have a way to set the custom URL for TWiML on the client --- temba/ivr/clients.py | 7 ++++++- temba/ivr/tests.py | 22 ++++++++++++++++++++++ temba/tests/twilio.py | 1 + 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/temba/ivr/clients.py b/temba/ivr/clients.py index 517cd80c920..7da5acd0062 100644 --- a/temba/ivr/clients.py +++ b/temba/ivr/clients.py @@ -5,6 +5,7 @@ from nexmo import AuthenticationError, ClientError, ServerError from twilio.base.exceptions import TwilioRestException from twilio.request_validator import RequestValidator +from twilio.rest.api import Api from django.conf import settings from django.urls import reverse @@ -126,9 +127,13 @@ def hangup(self, call): class TwilioClient(TembaTwilioRestClient): - def __init__(self, account_sid, token, org, **kwargs): + def __init__(self, account_sid, token, org, base=None, **kwargs): self.org = org super().__init__(account_sid, token, **kwargs) + custom_api = Api(self) + if base: + custom_api.base_url = base + self._api = custom_api def start_call(self, call, to, from_, status_callback): if not settings.SEND_CALLS: diff --git a/temba/ivr/tests.py b/temba/ivr/tests.py index 26c893f26e2..ff1057708c1 100644 --- a/temba/ivr/tests.py +++ b/temba/ivr/tests.py @@ -143,6 +143,26 @@ def create(self, to=None, from_=None, url=None, status_callback=None): log.text, "Call ended. Could not authenticate with your Twilio account. " "Check your token and try again." ) + def test_twiml_client(self): + # no twiml api config yet + self.assertIsNone(self.channel.get_twiml_client()) + + # twiml api config + config = { + Channel.CONFIG_SEND_URL: "https://api.twiml.com", + Channel.CONFIG_ACCOUNT_SID: "TEST_SID", + Channel.CONFIG_AUTH_TOKEN: "TEST_TOKEN", + } + channel = Channel.create( + self.org, self.org.get_user(), "BR", "TW", "+558299990000", "+558299990000", config, "AC" + ) + self.assertEqual(channel.org, self.org) + self.assertEqual(channel.address, "+558299990000") + + twiml_client = channel.get_twiml_client() + self.assertIsNotNone(twiml_client) + self.assertEqual(twiml_client.api.base_url, "https://api.twiml.com") + @patch("twilio.request_validator.RequestValidator", MockRequestValidator) @patch("twilio.rest.api.v2010.account.call.CallInstance", MockTwilioClient.MockCallInstance) def test_call_logging(self): @@ -1079,6 +1099,8 @@ def test_ivr_flow(self): self.assertEqual(channel.org, self.org) self.assertEqual(channel.address, "+558299990000") + self.assertIsNotNone(channel.get_twiml_client()) + # import an ivr flow self.import_file("call_me_maybe") diff --git a/temba/tests/twilio.py b/temba/tests/twilio.py index e08a83272e0..ba08153010b 100644 --- a/temba/tests/twilio.py +++ b/temba/tests/twilio.py @@ -45,6 +45,7 @@ def get(self, sid): class MockAPI(object): def __init__(self, *args, **kwargs): + self.base_url = "base_url" pass @property From e78667707dd493508d2d1a7fab4f7e1dbbe3d15b Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Wed, 29 Aug 2018 17:59:23 +0200 Subject: [PATCH 12/12] More coverage and move everything to set bare URL in the if block --- temba/channels/types/twilio/tests.py | 20 ++++++++++++-------- temba/ivr/clients.py | 4 ++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/temba/channels/types/twilio/tests.py b/temba/channels/types/twilio/tests.py index c2035ce77b2..9d62ce40b48 100644 --- a/temba/channels/types/twilio/tests.py +++ b/temba/channels/types/twilio/tests.py @@ -128,16 +128,20 @@ def test_claim(self): mock_short_codes.return_value = iter([]) Channel.objects.all().delete() - response = self.client.get(claim_twilio) - self.assertContains(response, "+55 41 3908-7835") + with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.get") as mock_numbers_get: + mock_numbers_get.return_value = MockTwilioClient.MockPhoneNumber("+554139087835") - # claim it - response = self.client.post(claim_twilio, dict(country="BR", phone_number="554139087835")) - self.assertRedirects(response, reverse("public.public_welcome") + "?success") + response = self.client.get(claim_twilio) + self.assertContains(response, "+55 41 3908-7835") - # make sure it is actually connected - channel = Channel.objects.get(channel_type="T", org=self.org) - self.assertEqual(channel.role, Channel.ROLE_CALL + Channel.ROLE_ANSWER) + # claim it + mock_numbers.return_value = iter([MockTwilioClient.MockPhoneNumber("+554139087835")]) + response = self.client.post(claim_twilio, dict(country="BR", phone_number="554139087835")) + self.assertRedirects(response, reverse("public.public_welcome") + "?success") + + # make sure it is actually connected + channel = Channel.objects.get(channel_type="T", org=self.org) + self.assertEqual(channel.role, Channel.ROLE_CALL + Channel.ROLE_ANSWER) with patch("temba.tests.twilio.MockTwilioClient.MockPhoneNumbers.stream") as mock_numbers: mock_numbers.return_value = iter([MockTwilioClient.MockPhoneNumber("+4545335500")]) diff --git a/temba/ivr/clients.py b/temba/ivr/clients.py index 7da5acd0062..85183118bc3 100644 --- a/temba/ivr/clients.py +++ b/temba/ivr/clients.py @@ -130,10 +130,10 @@ class TwilioClient(TembaTwilioRestClient): def __init__(self, account_sid, token, org, base=None, **kwargs): self.org = org super().__init__(account_sid, token, **kwargs) - custom_api = Api(self) if base: + custom_api = Api(self) custom_api.base_url = base - self._api = custom_api + self._api = custom_api def start_call(self, call, to, from_, status_callback): if not settings.SEND_CALLS: