Merge pull request #3493 from alphagov/reach-send-181665654

Add boilerplate for sending SMS via Reach
This commit is contained in:
Ben Thorner
2022-04-01 10:10:01 +01:00
committed by GitHub
20 changed files with 415 additions and 234 deletions

View File

@@ -29,6 +29,7 @@ export NOTIFY_ENVIRONMENT='development'
export MMG_API_KEY='MMG_API_KEY'
export FIRETEXT_API_KEY='FIRETEXT_ACTUAL_KEY'
export REACH_API_KEY='REACH_API_KEY'
export NOTIFICATION_QUEUE_PREFIX='YOUR_OWN_PREFIX'
export FLASK_APP=application.py
@@ -45,6 +46,7 @@ Things to change:
```
notify-pass credentials/firetext
notify-pass credentials/mmg
notify-pass credentials/reach
```
### Postgres

View File

@@ -36,6 +36,7 @@ from app.clients.email.aws_ses import AwsSesClient
from app.clients.email.aws_ses_stub import AwsSesStubClient
from app.clients.sms.firetext import FiretextClient
from app.clients.sms.mmg import MMGClient
from app.clients.sms.reach import ReachClient
class SQLAlchemy(_SQLAlchemy):
@@ -56,6 +57,7 @@ ma = Marshmallow()
notify_celery = NotifyCelery()
firetext_client = FiretextClient()
mmg_client = MMGClient()
reach_client = ReachClient()
aws_ses_client = AwsSesClient()
aws_ses_stub_client = AwsSesStubClient()
encryption = Encryption()
@@ -98,6 +100,7 @@ def create_app(application):
logging.init_app(application, statsd_client)
firetext_client.init_app(application, statsd_client=statsd_client)
mmg_client.init_app(application, statsd_client=statsd_client)
reach_client.init_app(application, statsd_client=statsd_client)
aws_ses_client.init_app(application.config['AWS_REGION'], statsd_client=statsd_client)
aws_ses_stub_client.init_app(
@@ -107,7 +110,10 @@ def create_app(application):
)
# If a stub url is provided for SES, then use the stub client rather than the real SES boto client
email_clients = [aws_ses_stub_client] if application.config['SES_STUB_URL'] else [aws_ses_client]
notification_provider_clients.init_app(sms_clients=[firetext_client, mmg_client], email_clients=email_clients)
notification_provider_clients.init_app(
sms_clients=[firetext_client, mmg_client, reach_client],
email_clients=email_clients
)
notify_celery.init_app(application)
encryption.init_app(application)

View File

@@ -24,7 +24,8 @@ def deliver_sms(self, notification_id):
except Exception as e:
if isinstance(e, SmsClientResponseException):
current_app.logger.warning(
"SMS notification delivery for id: {} failed".format(notification_id)
"SMS notification delivery for id: {} failed".format(notification_id),
exc_info=True
)
else:
current_app.logger.exception(

View File

@@ -28,5 +28,6 @@ class EmailClient(Client):
def send_email(self, *args, **kwargs):
raise NotImplementedError('TODO Need to implement.')
def get_name(self):
@property
def name(self):
raise NotImplementedError('TODO Need to implement.')

View File

@@ -59,11 +59,11 @@ class AwsSesClient(EmailClient):
def init_app(self, region, statsd_client, *args, **kwargs):
self._client = boto3.client('ses', region_name=region)
super(AwsSesClient, self).__init__(*args, **kwargs)
self.name = 'ses'
self.statsd_client = statsd_client
def get_name(self):
return self.name
@property
def name(self):
return 'ses'
def send_email(self,
source,

View File

@@ -13,12 +13,12 @@ class AwsSesStubClientException(EmailClientException):
class AwsSesStubClient(EmailClient):
def init_app(self, region, statsd_client, stub_url):
self.name = 'ses'
self.statsd_client = statsd_client
self.url = stub_url
def get_name(self):
return self.name
@property
def name(self):
return 'ses'
def send_email(self,
source,

View File

@@ -1,3 +1,5 @@
from time import monotonic
from app.clients import Client, ClientException
@@ -10,7 +12,7 @@ class SmsClientResponseException(ClientException):
self.message = message
def __str__(self):
return "Message {}".format(self.message)
return f"SMS client error ({self.message})"
class SmsClient(Client):
@@ -18,8 +20,52 @@ class SmsClient(Client):
Base Sms client for sending smss.
'''
def send_sms(self, *args, **kwargs):
def init_app(self, current_app, statsd_client):
self.current_app = current_app
self.statsd_client = statsd_client
self.from_number = self.current_app.config.get('FROM_NUMBER')
def record_outcome(self, success):
log_message = "Provider request for {} {}".format(
self.name,
"succeeded" if success else "failed",
)
if success:
self.current_app.logger.info(log_message)
self.statsd_client.incr(f"clients.{self.name}.success")
else:
self.statsd_client.incr(f"clients.{self.name}.error")
self.current_app.logger.warning(log_message)
def send_sms(self, to, content, reference, international, sender):
start_time = monotonic()
if sender is None:
# temporary log to see if the following ternary is necessary
# or if it's safe to remove it - keep for 1-2 weeks
self.current_app.logger.warning(
f"send_sms called with 'sender' of 'None' for {reference}"
)
sender = self.from_number if sender is None else sender
try:
response = self.try_send_sms(to, content, reference, international, sender)
self.record_outcome(True)
except SmsClientResponseException as e:
self.record_outcome(False)
raise e
finally:
elapsed_time = monotonic() - start_time
self.statsd_client.timing(f"clients.{self.name}.request-time", elapsed_time)
self.current_app.logger.info("Reach request for {} finished in {}".format(reference, elapsed_time))
return response
def try_send_sms(self, *args, **kwargs):
raise NotImplementedError('TODO Need to implement.')
def get_name(self):
@property
def name(self):
raise NotImplementedError('TODO Need to implement.')

View File

@@ -1,6 +1,5 @@
import json
import logging
from time import monotonic
from requests import RequestException, request
@@ -45,63 +44,30 @@ def get_message_status_and_reason_from_firetext_code(detailed_status_code):
return firetext_codes[detailed_status_code]['status'], firetext_codes[detailed_status_code]['reason']
class FiretextClientResponseException(SmsClientResponseException):
def __init__(self, response, exception):
status_code = response.status_code if response is not None else 504
text = response.text if response is not None else "Gateway Time-out"
self.status_code = status_code
self.text = text
self.exception = exception
def __str__(self):
return "Code {} text {} exception {}".format(self.status_code, self.text, str(self.exception))
class FiretextClient(SmsClient):
'''
FireText sms client.
'''
def init_app(self, current_app, statsd_client, *args, **kwargs):
super(SmsClient, self).__init__(*args, **kwargs)
self.current_app = current_app
self.api_key = current_app.config.get('FIRETEXT_API_KEY')
self.international_api_key = current_app.config.get('FIRETEXT_INTERNATIONAL_API_KEY')
self.from_number = current_app.config.get('FROM_NUMBER')
self.name = 'firetext'
self.url = current_app.config.get('FIRETEXT_URL')
self.statsd_client = statsd_client
def init_app(self, *args, **kwargs):
super().init_app(*args, **kwargs)
self.api_key = self.current_app.config.get('FIRETEXT_API_KEY')
self.international_api_key = self.current_app.config.get('FIRETEXT_INTERNATIONAL_API_KEY')
self.url = self.current_app.config.get('FIRETEXT_URL')
def get_name(self):
return self.name
@property
def name(self):
return 'firetext'
def record_outcome(self, success, response):
status_code = response.status_code if response else 503
log_message = "API {} request {} on {} response status_code {}".format(
"POST",
"succeeded" if success else "failed",
self.url,
status_code
)
if success:
self.current_app.logger.info(log_message)
self.statsd_client.incr("clients.firetext.success")
else:
self.statsd_client.incr("clients.firetext.error")
self.current_app.logger.warning(log_message)
def send_sms(self, to, content, reference, international, sender=None):
def try_send_sms(self, to, content, reference, international, sender):
data = {
"apiKey": self.international_api_key if international else self.api_key,
"from": self.from_number if sender is None else sender,
"from": sender,
"to": to.replace('+', ''),
"message": content,
"reference": reference
}
start_time = monotonic()
try:
response = request(
"POST",
@@ -113,16 +79,10 @@ class FiretextClient(SmsClient):
try:
json.loads(response.text)
if response.json()['code'] != 0:
raise ValueError()
except (ValueError, AttributeError) as e:
self.record_outcome(False, response)
raise FiretextClientResponseException(response=response, exception=e)
self.record_outcome(True, response)
except RequestException as e:
self.record_outcome(False, e.response)
raise FiretextClientResponseException(response=e.response, exception=e)
finally:
elapsed_time = monotonic() - start_time
self.current_app.logger.info("Firetext request for {} finished in {}".format(reference, elapsed_time))
self.statsd_client.timing("clients.firetext.request-time", elapsed_time)
raise ValueError("Expected 'code' to be '0'")
except (ValueError, AttributeError):
raise SmsClientResponseException("Invalid response JSON")
except RequestException:
raise SmsClientResponseException("Request failed")
return response

View File

@@ -1,5 +1,4 @@
import json
from time import monotonic
from requests import RequestException, request
@@ -69,45 +68,25 @@ class MMGClient(SmsClient):
MMG sms client
'''
def init_app(self, current_app, statsd_client, *args, **kwargs):
super(SmsClient, self).__init__(*args, **kwargs)
self.current_app = current_app
self.api_key = current_app.config.get('MMG_API_KEY')
self.from_number = current_app.config.get('FROM_NUMBER')
self.name = 'mmg'
self.statsd_client = statsd_client
self.mmg_url = current_app.config.get('MMG_URL')
def init_app(self, *args, **kwargs):
super().init_app(*args, **kwargs)
self.api_key = self.current_app.config.get('MMG_API_KEY')
self.mmg_url = self.current_app.config.get('MMG_URL')
def record_outcome(self, success, response):
status_code = response.status_code if response else 503
log_message = "API {} request {} on {} response status_code {}".format(
"POST",
"succeeded" if success else "failed",
self.mmg_url,
status_code
)
@property
def name(self):
return 'mmg'
if success:
self.current_app.logger.info(log_message)
self.statsd_client.incr("clients.mmg.success")
else:
self.statsd_client.incr("clients.mmg.error")
self.current_app.logger.warning(log_message)
def get_name(self):
return self.name
def send_sms(self, to, content, reference, international, multi=True, sender=None):
def try_send_sms(self, to, content, reference, international, sender):
data = {
"reqType": "BULK",
"MSISDN": to,
"msg": content,
"sender": self.from_number if sender is None else sender,
"sender": sender,
"cid": reference,
"multi": multi
"multi": True
}
start_time = monotonic()
try:
response = request(
"POST",
@@ -123,16 +102,9 @@ class MMGClient(SmsClient):
response.raise_for_status()
try:
json.loads(response.text)
except (ValueError, AttributeError) as e:
self.record_outcome(False, response)
raise MMGClientResponseException(response=response, exception=e)
self.record_outcome(True, response)
except RequestException as e:
self.record_outcome(False, e.response)
raise MMGClientResponseException(response=e.response, exception=e)
finally:
elapsed_time = monotonic() - start_time
self.statsd_client.timing("clients.mmg.request-time", elapsed_time)
self.current_app.logger.info("MMG request for {} finished in {}".format(reference, elapsed_time))
except (ValueError, AttributeError):
raise SmsClientResponseException("Invalid response JSON")
except RequestException:
raise SmsClientResponseException("Request failed")
return response

View File

@@ -1,3 +1,7 @@
import json
from requests import RequestException, request
from app.clients.sms import SmsClient, SmsClientResponseException
@@ -12,14 +16,37 @@ def get_reach_responses(status, detailed_status_code=None):
raise KeyError
class ReachClientResponseException(SmsClientResponseException):
pass # TODO (custom exception for errors)
class ReachClient(SmsClient):
def init_app(self, *args, **kwargs):
super().init_app(*args, **kwargs)
self.url = self.current_app.config.get('REACH_URL')
def get_name(self):
pass # TODO
@property
def name(self):
return 'reach'
def send_sms(self, to, content, reference, international, multi=True, sender=None):
pass # TODO
def try_send_sms(self, to, content, reference, international, sender):
data = {
# TODO
}
try:
response = request(
"POST",
self.url,
data=json.dumps(data),
headers={
'Content-Type': 'application/json',
},
timeout=60
)
response.raise_for_status()
try:
json.loads(response.text)
except (ValueError, AttributeError):
raise SmsClientResponseException("Invalid response JSON")
except RequestException:
raise SmsClientResponseException("Request failed")
return response

View File

@@ -380,6 +380,7 @@ class Config(object):
# these environment vars aren't defined in the manifest so to set them on paas use `cf set-env`
MMG_URL = os.environ.get("MMG_URL", "https://api.mmg.co.uk/jsonv2a/api.php")
FIRETEXT_URL = os.environ.get("FIRETEXT_URL", "https://www.firetext.co.uk/api/sendsms/json")
REACH_URL = os.environ.get("REACH_URL", "TODO")
SES_STUB_URL = os.environ.get("SES_STUB_URL")
AWS_REGION = 'eu-west-1'
@@ -481,6 +482,7 @@ class Test(Development):
MMG_URL = 'https://example.com/mmg'
FIRETEXT_URL = 'https://example.com/firetext'
REACH_URL = 'https://example.com/reach'
CBC_PROXY_ENABLED = True
DVLA_EMAIL_ADDRESSES = ['success@simulator.amazonses.com', 'success+2@simulator.amazonses.com']

View File

@@ -60,7 +60,7 @@ def send_sms_to_provider(notification):
key_type = notification.key_type
if service.research_mode or notification.key_type == KEY_TYPE_TEST:
update_notification_to_sending(notification, provider)
send_sms_response(provider.get_name(), str(notification.id), notification.to)
send_sms_response(provider.name, str(notification.id), notification.to)
else:
try:
@@ -82,7 +82,7 @@ def send_sms_to_provider(notification):
except Exception as e:
notification.billable_units = template.fragment_count
dao_update_notification(notification)
dao_reduce_sms_provider_priority(provider.get_name(), time_threshold=timedelta(minutes=1))
dao_reduce_sms_provider_priority(provider.name, time_threshold=timedelta(minutes=1))
raise e
else:
notification.billable_units = template.fragment_count
@@ -158,7 +158,7 @@ def send_email_to_provider(notification):
def update_notification_to_sending(notification, provider):
notification.sent_at = datetime.utcnow()
notification.sent_by = provider.get_name()
notification.sent_by = provider.name
if notification.status not in NOTIFICATION_STATUS_TYPES_COMPLETED:
notification.status = NOTIFICATION_SENT if notification.international else NOTIFICATION_SENDING
dao_update_notification(notification)

View File

@@ -0,0 +1,54 @@
"""
Revision ID: 0367_add_reach
Revises: 0366_letter_rates_2022
Create Date: 2022-03-24 16:00:00
"""
import itertools
import uuid
from datetime import datetime
from alembic import op
from sqlalchemy.sql import text
from app.models import LetterRate
revision = '0367_add_reach'
down_revision = '0366_letter_rates_2022'
def upgrade():
conn = op.get_bind()
conn.execute(
"""
INSERT INTO provider_details (
id,
display_name,
identifier,
priority,
notification_type,
active,
version,
created_by_id
)
VALUES (
'{}',
'Reach',
'reach',
0,
'sms',
false,
1,
null
)
""".format(
str(uuid.uuid4()),
)
)
def downgrade():
conn = op.get_bind()
conn.execute("DELETE FROM provider_details WHERE identifier = 'reach'")

View File

@@ -2,11 +2,9 @@ from urllib.parse import parse_qs
import pytest
import requests_mock
from requests import HTTPError
from requests.exceptions import ConnectTimeout, ReadTimeout
from app.clients.sms.firetext import (
FiretextClientResponseException,
SmsClientResponseException,
get_firetext_responses,
)
@@ -36,7 +34,7 @@ def test_get_firetext_responses_raises_KeyError_if_unrecognised_status_code():
assert '99' in str(e.value)
def test_send_sms_successful_returns_firetext_response(mocker, mock_firetext_client):
def test_try_send_sms_successful_returns_firetext_response(mocker, mock_firetext_client):
to = content = reference = 'foo'
response_dict = {
'data': [],
@@ -47,7 +45,7 @@ def test_send_sms_successful_returns_firetext_response(mocker, mock_firetext_cli
with requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/firetext', json=response_dict, status_code=200)
response = mock_firetext_client.send_sms(to, content, reference, False)
response = mock_firetext_client.try_send_sms(to, content, reference, False, 'sender')
response_json = response.json()
assert response.status_code == 200
@@ -55,7 +53,7 @@ def test_send_sms_successful_returns_firetext_response(mocker, mock_firetext_cli
assert response_json['description'] == 'SMS successfully queued'
def test_send_sms_calls_firetext_correctly(mocker, mock_firetext_client):
def test_try_send_sms_calls_firetext_correctly(mocker, mock_firetext_client):
to = '+447234567890'
content = 'my message'
reference = 'my reference'
@@ -65,7 +63,7 @@ def test_send_sms_calls_firetext_correctly(mocker, mock_firetext_client):
with requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/firetext', json=response_dict, status_code=200)
mock_firetext_client.send_sms(to, content, reference, False)
mock_firetext_client.try_send_sms(to, content, reference, False, 'bar')
assert request_mock.call_count == 1
assert request_mock.request_history[0].url == 'https://example.com/firetext'
@@ -79,7 +77,7 @@ def test_send_sms_calls_firetext_correctly(mocker, mock_firetext_client):
assert request_args['reference'][0] == reference
def test_send_sms_calls_firetext_correctly_for_international(mocker, mock_firetext_client):
def test_try_send_sms_calls_firetext_correctly_for_international(mocker, mock_firetext_client):
to = '+607234567890'
content = 'my message'
reference = 'my reference'
@@ -89,7 +87,7 @@ def test_send_sms_calls_firetext_correctly_for_international(mocker, mock_firete
with requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/firetext', json=response_dict, status_code=200)
mock_firetext_client.send_sms(to, content, reference, True)
mock_firetext_client.try_send_sms(to, content, reference, True, 'bar')
assert request_mock.call_count == 1
assert request_mock.request_history[0].url == 'https://example.com/firetext'
@@ -103,7 +101,7 @@ def test_send_sms_calls_firetext_correctly_for_international(mocker, mock_firete
assert request_args['reference'][0] == reference
def test_send_sms_raises_if_firetext_rejects(mocker, mock_firetext_client):
def test_try_send_sms_raises_if_firetext_rejects(mocker, mock_firetext_client):
to = content = reference = 'foo'
response_dict = {
'data': [],
@@ -114,60 +112,48 @@ def test_send_sms_raises_if_firetext_rejects(mocker, mock_firetext_client):
with pytest.raises(SmsClientResponseException) as exc, requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/firetext', json=response_dict, status_code=200)
mock_firetext_client.send_sms(to, content, reference, False)
mock_firetext_client.try_send_sms(to, content, reference, False, 'sender')
assert exc.value.status_code == 200
assert '"description": "Some kind of error"' in exc.value.text
assert '"code": 1' in exc.value.text
assert "Invalid response JSON" in str(exc.value)
def test_send_sms_raises_if_firetext_rejects_with_unexpected_data(mocker, mock_firetext_client):
def test_try_send_sms_raises_if_firetext_rejects_with_unexpected_data(mocker, mock_firetext_client):
to = content = reference = 'foo'
response_dict = {"something": "gone bad"}
with pytest.raises(SmsClientResponseException) as exc, requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/firetext', json=response_dict, status_code=400)
mock_firetext_client.send_sms(to, content, reference, False)
mock_firetext_client.try_send_sms(to, content, reference, False, 'sender')
assert exc.value.status_code == 400
assert exc.value.text == '{"something": "gone bad"}'
assert type(exc.value.exception) == HTTPError
assert "Request failed" in str(exc.value)
def test_send_sms_override_configured_shortcode_with_sender(mocker, mock_firetext_client):
to = '+447234567890'
content = 'my message'
reference = 'my reference'
response_dict = {
'code': 0,
}
sender = 'fromservice'
def test_try_send_sms_raises_if_firetext_fails_to_return_json(notify_api, mock_firetext_client):
to = content = reference = 'foo'
response_dict = 'NOT AT ALL VALID JSON {"key" : "value"}}'
with requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/firetext', json=response_dict, status_code=200)
mock_firetext_client.send_sms(to, content, reference, False, sender=sender)
with pytest.raises(SmsClientResponseException) as exc, requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/firetext', text=response_dict, status_code=200)
mock_firetext_client.try_send_sms(to, content, reference, False, 'sender')
request_args = parse_qs(request_mock.request_history[0].text)
assert request_args['from'][0] == 'fromservice'
assert "Invalid response JSON" in str(exc.value)
def test_send_sms_raises_if_firetext_rejects_with_connect_timeout(rmock, mock_firetext_client):
def test_try_send_sms_raises_if_firetext_rejects_with_connect_timeout(rmock, mock_firetext_client):
to = content = reference = 'foo'
with pytest.raises(FiretextClientResponseException) as exc:
with pytest.raises(SmsClientResponseException) as exc:
rmock.register_uri('POST', 'https://example.com/firetext', exc=ConnectTimeout)
mock_firetext_client.send_sms(to, content, reference, False)
mock_firetext_client.try_send_sms(to, content, reference, False, 'sender')
assert exc.value.status_code == 504
assert exc.value.text == 'Gateway Time-out'
assert "Request failed" in str(exc.value)
def test_send_sms_raises_if_firetext_rejects_with_read_timeout(rmock, mock_firetext_client):
def test_try_send_sms_raises_if_firetext_rejects_with_read_timeout(rmock, mock_firetext_client):
to = content = reference = 'foo'
with pytest.raises(FiretextClientResponseException) as exc:
with pytest.raises(SmsClientResponseException) as exc:
rmock.register_uri('POST', 'https://example.com/firetext', exc=ReadTimeout)
mock_firetext_client.send_sms(to, content, reference, False)
mock_firetext_client.try_send_sms(to, content, reference, False, 'sender')
assert exc.value.status_code == 504
assert exc.value.text == 'Gateway Time-out'
assert "Request failed" in str(exc.value)

View File

@@ -1,11 +1,9 @@
import pytest
import requests_mock
from requests import HTTPError
from requests.exceptions import ConnectTimeout, ReadTimeout
from app import mmg_client
from app.clients.sms import SmsClientResponseException
from app.clients.sms.mmg import MMGClientResponseException, get_mmg_responses
from app.clients.sms.mmg import SmsClientResponseException, get_mmg_responses
@pytest.mark.parametrize('detailed_status_code, result', [
@@ -38,20 +36,20 @@ def test_get_mmg_responses_raises_KeyError_if_unrecognised_status_code():
assert '99' in str(e.value)
def test_send_sms_successful_returns_mmg_response(notify_api, mocker):
def test_try_send_sms_successful_returns_mmg_response(notify_api, mocker):
to = content = reference = 'foo'
response_dict = {'Reference': 12345678}
with requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/mmg', json=response_dict, status_code=200)
response = mmg_client.send_sms(to, content, reference, False)
response = mmg_client.try_send_sms(to, content, reference, False, 'sender')
response_json = response.json()
assert response.status_code == 200
assert response_json['Reference'] == 12345678
def test_send_sms_calls_mmg_correctly(notify_api, mocker):
def test_try_send_sms_calls_mmg_correctly(notify_api, mocker):
to = '+447234567890'
content = 'my message'
reference = 'my reference'
@@ -59,7 +57,7 @@ def test_send_sms_calls_mmg_correctly(notify_api, mocker):
with requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/mmg', json=response_dict, status_code=200)
mmg_client.send_sms(to, content, reference, False)
mmg_client.try_send_sms(to, content, reference, False, 'testing')
assert request_mock.call_count == 1
assert request_mock.request_history[0].url == 'https://example.com/mmg'
@@ -74,7 +72,7 @@ def test_send_sms_calls_mmg_correctly(notify_api, mocker):
assert request_args['multi'] is True
def test_send_sms_raises_if_mmg_rejects(notify_api, mocker):
def test_try_send_sms_raises_if_mmg_rejects(notify_api, mocker):
to = content = reference = 'foo'
response_dict = {
'Error': 206,
@@ -83,59 +81,37 @@ def test_send_sms_raises_if_mmg_rejects(notify_api, mocker):
with pytest.raises(SmsClientResponseException) as exc, requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/mmg', json=response_dict, status_code=400)
mmg_client.send_sms(to, content, reference, False)
mmg_client.try_send_sms(to, content, reference, False, 'sender')
assert exc.value.status_code == 400
assert '"Error": 206' in exc.value.text
assert '"Description": "Some kind of error"' in exc.value.text
assert type(exc.value.exception) == HTTPError
assert "Request failed" in str(exc.value)
def test_send_sms_override_configured_shortcode_with_sender(notify_api, mocker):
to = '+447234567890'
content = 'my message'
reference = 'my reference'
response_dict = {'Reference': 12345678}
sender = 'fromservice'
with requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/mmg', json=response_dict, status_code=200)
mmg_client.send_sms(to, content, reference, False, sender=sender)
request_args = request_mock.request_history[0].json()
assert request_args['sender'] == 'fromservice'
def test_send_sms_raises_if_mmg_fails_to_return_json(notify_api, mocker):
def test_try_send_sms_raises_if_mmg_fails_to_return_json(notify_api, mocker):
to = content = reference = 'foo'
response_dict = 'NOT AT ALL VALID JSON {"key" : "value"}}'
with pytest.raises(SmsClientResponseException) as exc, requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/mmg', text=response_dict, status_code=200)
mmg_client.send_sms(to, content, reference, False)
mmg_client.try_send_sms(to, content, reference, False, 'sender')
assert 'Code 200 text NOT AT ALL VALID JSON {"key" : "value"}} exception Expecting value: line 1 column 1 (char 0)' in str(exc.value) # noqa
assert exc.value.status_code == 200
assert exc.value.text == 'NOT AT ALL VALID JSON {"key" : "value"}}'
assert "Invalid response JSON" in str(exc.value)
def test_send_sms_raises_if_mmg_rejects_with_connect_timeout(rmock):
def test_try_send_sms_raises_if_mmg_rejects_with_connect_timeout(rmock):
to = content = reference = 'foo'
with pytest.raises(MMGClientResponseException) as exc:
with pytest.raises(SmsClientResponseException) as exc:
rmock.register_uri('POST', 'https://example.com/mmg', exc=ConnectTimeout)
mmg_client.send_sms(to, content, reference, False)
mmg_client.try_send_sms(to, content, reference, False, 'sender')
assert exc.value.status_code == 504
assert exc.value.text == 'Gateway Time-out'
assert "Request failed" in str(exc.value)
def test_send_sms_raises_if_mmg_rejects_with_read_timeout(rmock):
def test_try_send_sms_raises_if_mmg_rejects_with_read_timeout(rmock):
to = content = reference = 'foo'
with pytest.raises(MMGClientResponseException) as exc:
with pytest.raises(SmsClientResponseException) as exc:
rmock.register_uri('POST', 'https://example.com/mmg', exc=ReadTimeout)
mmg_client.send_sms(to, content, reference, False)
mmg_client.try_send_sms(to, content, reference, False, 'sender')
assert exc.value.status_code == 504
assert exc.value.text == 'Gateway Time-out'
assert "Request failed" in str(exc.value)

View File

@@ -1 +1,84 @@
# TODO: all of the tests
import pytest
import requests_mock
from requests.exceptions import ConnectTimeout, ReadTimeout
from app import reach_client
from app.clients.sms import SmsClientResponseException
# TODO: tests for get_reach_responses
def test_try_send_sms_successful_returns_reach_response(notify_api, mocker):
to = content = reference = 'foo'
response_dict = {} # TODO
with requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/reach', json=response_dict, status_code=200)
response = reach_client.try_send_sms(to, content, reference, False, 'sender')
# response_json = response.json()
assert response.status_code == 200
# TODO: assertions
def test_try_send_sms_calls_reach_correctly(notify_api, mocker):
to = '+447234567890'
content = 'my message'
reference = 'my reference'
response_dict = {} # TODO
with requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/reach', json=response_dict, status_code=200)
reach_client.try_send_sms(to, content, reference, False, 'sender')
assert request_mock.call_count == 1
assert request_mock.request_history[0].url == 'https://example.com/reach'
assert request_mock.request_history[0].method == 'POST'
# request_args = request_mock.request_history[0].json()
# TODO: assertions
def test_try_send_sms_raises_if_reach_rejects(notify_api, mocker):
to = content = reference = 'foo'
response_dict = {
'Error': 206,
'Description': 'Some kind of error'
}
with pytest.raises(SmsClientResponseException) as exc, requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/reach', json=response_dict, status_code=400)
reach_client.try_send_sms(to, content, reference, False, 'sender')
assert "Request failed" in str(exc)
def test_try_send_sms_raises_if_reach_fails_to_return_json(notify_api, mocker):
to = content = reference = 'foo'
response_dict = 'NOT AT ALL VALID JSON {"key" : "value"}}'
with pytest.raises(SmsClientResponseException) as exc, requests_mock.Mocker() as request_mock:
request_mock.post('https://example.com/reach', text=response_dict, status_code=200)
reach_client.try_send_sms(to, content, reference, False, 'sender')
assert 'Invalid response JSON' in str(exc.value)
def test_try_send_sms_raises_if_reach_rejects_with_connect_timeout(rmock):
to = content = reference = 'foo'
with pytest.raises(SmsClientResponseException) as exc:
rmock.register_uri('POST', 'https://example.com/reach', exc=ConnectTimeout)
reach_client.try_send_sms(to, content, reference, False, 'sender')
assert 'Request failed' in str(exc.value)
def test_try_send_sms_raises_if_reach_rejects_with_read_timeout(rmock):
to = content = reference = 'foo'
with pytest.raises(SmsClientResponseException) as exc:
rmock.register_uri('POST', 'https://example.com/reach', exc=ReadTimeout)
reach_client.try_send_sms(to, content, reference, False, 'sender')
assert 'Request failed' in str(exc.value)

View File

@@ -0,0 +1,66 @@
import pytest
from app import statsd_client
from app.clients.sms import SmsClient, SmsClientResponseException
@pytest.fixture
def fake_client(notify_api):
class FakeSmsClient(SmsClient):
@property
def name(self):
return 'fake'
fake_client = FakeSmsClient()
fake_client.init_app(notify_api, statsd_client)
return fake_client
def test_send_sms(fake_client, mocker):
mock_send = mocker.patch.object(fake_client, 'try_send_sms')
fake_client.send_sms(
to='to',
content='content',
reference='reference',
international=False,
sender=None,
)
mock_send.assert_called_with(
'to', 'content', 'reference', False, 'testing'
)
def test_send_sms_error(fake_client, mocker):
mocker.patch.object(
fake_client, 'try_send_sms', side_effect=SmsClientResponseException('error')
)
with pytest.raises(SmsClientResponseException):
fake_client.send_sms(
to='to',
content='content',
reference='reference',
international=False,
sender=None,
)
def test_send_sms_override_configured_shortcode_with_sender(
fake_client,
mocker
):
mock_send = mocker.patch.object(fake_client, 'try_send_sms')
fake_client.send_sms(
to='to',
content='content',
reference='reference',
international=False,
sender='sender'
)
mock_send.assert_called_with(
'to', 'content', 'reference', False, 'sender'
)

View File

@@ -40,7 +40,7 @@ def set_primary_sms_provider(identifier):
def test_can_get_sms_non_international_providers(notify_db_session):
sms_providers = get_provider_details_by_notification_type('sms')
assert len(sms_providers) == 2
assert len(sms_providers) > 0
assert all('sms' == prov.notification_type for prov in sms_providers)
@@ -53,8 +53,8 @@ def test_can_get_sms_international_providers(notify_db_session):
def test_can_get_sms_providers_in_order_of_priority(notify_db_session):
providers = get_provider_details_by_notification_type('sms', False)
assert providers[0].priority < providers[1].priority
priorities = [provider.priority for provider in providers]
assert priorities == sorted(priorities)
def test_can_get_email_providers_in_order_of_priority(notify_db_session):
@@ -332,27 +332,26 @@ def test_dao_get_provider_stats(notify_db_session):
create_ft_billing('2018-06-15', sms_template_1, provider='firetext', billable_unit=1)
create_ft_billing('2018-06-28', sms_template_2, provider='mmg', billable_unit=2)
result = dao_get_provider_stats()
results = dao_get_provider_stats()
assert len(result) == 4
assert len(results) > 0
assert result[0].identifier == 'ses'
assert result[0].display_name == 'AWS SES'
assert result[0].created_by_name is None
assert result[0].current_month_billable_sms == 0
ses = next(result for result in results if result.identifier == 'ses')
firetext = next(result for result in results if result.identifier == 'firetext')
mmg = next(result for result in results if result.identifier == 'mmg')
assert result[1].identifier == 'firetext'
assert result[1].notification_type == 'sms'
assert result[1].supports_international is False
assert result[1].active is True
assert result[1].current_month_billable_sms == 5
assert ses.display_name == 'AWS SES'
assert ses.created_by_name is None
assert ses.current_month_billable_sms == 0
assert result[2].identifier == 'mmg'
assert result[2].display_name == 'MMG'
assert result[2].supports_international is True
assert result[2].active is True
assert result[2].current_month_billable_sms == 4
assert firetext.display_name == 'Firetext'
assert firetext.notification_type == 'sms'
assert firetext.supports_international is False
assert firetext.active is True
assert firetext.current_month_billable_sms == 5
assert result[3].identifier == 'dvla'
assert result[3].current_month_billable_sms == 0
assert result[3].supports_international is False
assert mmg.identifier == 'mmg'
assert mmg.display_name == 'MMG'
assert mmg.supports_international is True
assert mmg.active is True
assert mmg.current_month_billable_sms == 4

View File

@@ -54,7 +54,7 @@ def test_provider_to_use_should_return_random_provider(mocker, notify_db_session
ret = send_to_providers.provider_to_use('sms', international=False)
mock_choices.assert_called_once_with([mmg, firetext], weights=[25, 75])
assert ret.get_name() == 'mmg'
assert ret.name == 'mmg'
def test_provider_to_use_should_cache_repeated_calls(mocker, notify_db_session):
@@ -89,7 +89,7 @@ def test_provider_to_use_should_only_return_mmg_for_international(
ret = send_to_providers.provider_to_use('sms', international=True)
mock_choices.assert_called_once_with([mmg], weights=[100])
assert ret.get_name() == 'mmg'
assert ret.name == 'mmg'
def test_provider_to_use_should_only_return_active_providers(mocker, restore_provider_details):
@@ -101,7 +101,7 @@ def test_provider_to_use_should_only_return_active_providers(mocker, restore_pro
ret = send_to_providers.provider_to_use('sms')
mock_choices.assert_called_once_with([firetext], weights=[100])
assert ret.get_name() == 'firetext'
assert ret.name == 'firetext'
def test_provider_to_use_raises_if_no_active_providers(mocker, restore_provider_details):

View File

@@ -10,8 +10,8 @@ from tests.app.db import create_ft_billing
def test_get_provider_details_returns_all_providers(admin_request, notify_db_session):
json_resp = admin_request.get('provider_details.get_providers')['provider_details']
assert len(json_resp) == 4
assert {x['identifier'] for x in json_resp} == {'ses', 'firetext', 'mmg', 'dvla'}
assert len(json_resp) > 0
assert {'ses', 'firetext', 'mmg', 'dvla'} < {x['identifier'] for x in json_resp}
def test_get_provider_details_by_id(client, notify_db):
@@ -45,7 +45,7 @@ def test_get_provider_contains_correct_fields(client, sample_template):
"active", "updated_at", "supports_international",
"current_month_billable_sms"
}
assert len(json_resp) == 4
assert len(json_resp) > 0
assert allowed_keys == set(json_resp[0].keys())