mirror of
https://github.com/GSA/notifications-api.git
synced 2025-12-09 06:32:11 -05:00
Add boilerplate for sending SMS via Reach
This works in conjunction with the new SMS provider stub [^1].
Local testing:
- Run the migrations to add Reach as an inactive provider.
- Activate the Reach provider locally and deactivate the others.
update provider_details set priority = 100, active = false where notification_type = 'sms';
update provider_details set active = true where identifier = 'reach';
- Tweak your local environment to point at the SMS stub.
export REACH_URL="http://host.docker.internal:6300/reach"
- Start / restart Celery to pick up the config change.
- Send a SMS via the Admin app and see the stub log it.
- Reset your environment so you can send normal SMS.
update provider_details set active = true where notification_type = 'sms';
update provider_details set active = false where identifier = 'reach';
[^1]: https://github.com/alphagov/notifications-sms-provider-stub/pull/10
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import json
|
||||
|
||||
from requests import RequestException, request
|
||||
|
||||
from app.clients.sms import SmsClient, SmsClientResponseException
|
||||
|
||||
|
||||
@@ -13,13 +17,49 @@ def get_reach_responses(status, detailed_status_code=None):
|
||||
|
||||
|
||||
class ReachClientResponseException(SmsClientResponseException):
|
||||
pass # TODO (custom exception for errors)
|
||||
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 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) as e:
|
||||
raise ReachClientResponseException(response=response, exception=e)
|
||||
except RequestException as e:
|
||||
raise ReachClientResponseException(response=e.response, exception=e)
|
||||
|
||||
return response
|
||||
|
||||
@@ -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']
|
||||
|
||||
54
migrations/versions/0367_add_reach.py
Normal file
54
migrations/versions/0367_add_reach.py
Normal 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'")
|
||||
@@ -1 +1,93 @@
|
||||
# TODO: all of the tests
|
||||
import pytest
|
||||
import requests_mock
|
||||
from requests import HTTPError
|
||||
from requests.exceptions import ConnectTimeout, ReadTimeout
|
||||
|
||||
from app import reach_client
|
||||
from app.clients.sms import SmsClientResponseException
|
||||
from app.clients.sms.reach import ReachClientResponseException
|
||||
|
||||
# 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 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
|
||||
|
||||
|
||||
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 '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"}}'
|
||||
|
||||
|
||||
def test_try_send_sms_raises_if_reach_rejects_with_connect_timeout(rmock):
|
||||
to = content = reference = 'foo'
|
||||
|
||||
with pytest.raises(ReachClientResponseException) as exc:
|
||||
rmock.register_uri('POST', 'https://example.com/reach', exc=ConnectTimeout)
|
||||
reach_client.try_send_sms(to, content, reference, False, 'sender')
|
||||
|
||||
assert exc.value.status_code == 504
|
||||
assert exc.value.text == 'Gateway Time-out'
|
||||
|
||||
|
||||
def test_try_send_sms_raises_if_reach_rejects_with_read_timeout(rmock):
|
||||
to = content = reference = 'foo'
|
||||
|
||||
with pytest.raises(ReachClientResponseException) as exc:
|
||||
rmock.register_uri('POST', 'https://example.com/reach', exc=ReadTimeout)
|
||||
reach_client.try_send_sms(to, content, reference, False, 'sender')
|
||||
|
||||
assert exc.value.status_code == 504
|
||||
assert exc.value.text == 'Gateway Time-out'
|
||||
|
||||
Reference in New Issue
Block a user