Merge pull request #3492 from alphagov/reach-basics-181665654

Add boilerplate for Reach SMS callbacks
This commit is contained in:
Ben Thorner
2022-03-29 16:32:12 +01:00
committed by GitHub
8 changed files with 179 additions and 118 deletions

View File

@@ -8,6 +8,7 @@ from app import notify_celery, statsd_client
from app.clients import ClientException
from app.clients.sms.firetext import get_firetext_responses
from app.clients.sms.mmg import get_mmg_responses
from app.clients.sms.reach import get_reach_responses
from app.dao import notifications_dao
from app.dao.templates_dao import dao_get_template_by_id
from app.models import NOTIFICATION_PENDING
@@ -17,7 +18,8 @@ from app.notifications.notifications_ses_callback import (
sms_response_mapper = {
'MMG': get_mmg_responses,
'Firetext': get_firetext_responses
'Firetext': get_firetext_responses,
'Reach': get_reach_responses
}

25
app/clients/sms/reach.py Normal file
View File

@@ -0,0 +1,25 @@
from app.clients.sms import SmsClient, SmsClientResponseException
def get_reach_responses(status, detailed_status_code=None):
if status == 'TODO-d':
return ("delivered", "TODO: Delivered")
elif status == 'TODO-tf':
return ("temporary-failure", "TODO: Temporary failure")
elif status == 'TODO-pf':
return ("permanent-failure", "TODO: Permanent failure")
else:
raise KeyError
class ReachClientResponseException(SmsClientResponseException):
pass # TODO (custom exception for errors)
class ReachClient(SmsClient):
def get_name(self):
pass # TODO
def send_sms(self, to, content, reference, international, multi=True, sender=None):
pass # TODO

View File

@@ -1,4 +1,4 @@
from flask import Blueprint, current_app, json, jsonify, request
from flask import Blueprint, json, jsonify, request
from app.celery.process_sms_client_response_tasks import (
process_sms_client_response,
@@ -30,12 +30,6 @@ def process_mmg_response():
queue=QueueNames.SMS_CALLBACKS,
)
safe_to_log = data.copy()
safe_to_log.pop("MSISDN")
current_app.logger.debug(
f"Full delivery response from {client_name} for notification: {provider_reference}\n{safe_to_log}"
)
return jsonify(result='success'), 200
@@ -52,12 +46,28 @@ def process_firetext_response():
detailed_status_code = request.form.get('code')
provider_reference = request.form.get('reference')
safe_to_log = dict(request.form).copy()
safe_to_log.pop('mobile')
current_app.logger.debug(
f"Full delivery response from {client_name} for notification: {provider_reference}\n{safe_to_log}"
process_sms_client_response.apply_async(
[status, provider_reference, client_name, detailed_status_code],
queue=QueueNames.SMS_CALLBACKS,
)
return jsonify(result='success'), 200
@sms_callback_blueprint.route('/reach', methods=['POST'])
def process_reach_response():
client_name = 'Reach'
# TODO: validate request
errors = None
if errors:
raise InvalidRequest(errors, status_code=400)
status = 'TODO-d' # TODO
detailed_status_code = 'something' # TODO
provider_reference = 'notification_id' # TODO
process_sms_client_response.apply_async(
[status, provider_reference, client_name, detailed_status_code],
queue=QueueNames.SMS_CALLBACKS,

View File

@@ -27,9 +27,9 @@
'STATSD_HOST': None
},
'routes': {
'preview': ['api.notify.works/notifications/sms/mmg', 'api.notify.works/notifications/sms/firetext'],
'staging': ['api.staging-notify.works/notifications/sms/mmg', 'api.staging-notify.works/notifications/sms/firetext'],
'production': ['api.notifications.service.gov.uk/notifications/sms/mmg', 'api.notifications.service.gov.uk/notifications/sms/firetext'],
'preview': ['api.notify.works/notifications/sms/mmg', 'api.notify.works/notifications/sms/firetext', 'api.notify.works/notifications/sms/reach'],
'staging': ['api.staging-notify.works/notifications/sms/mmg', 'api.staging-notify.works/notifications/sms/firetext', 'api.staging-notify.works/notifications/sms/reach'],
'production': ['api.notifications.service.gov.uk/notifications/sms/mmg', 'api.notifications.service.gov.uk/notifications/sms/firetext', 'api.notifications.service.gov.uk/notifications/sms/reach'],
},
'health-check-type': 'port',
'health-check-invocation-timeout': 3,

View File

@@ -18,7 +18,7 @@ def test_process_sms_client_response_raises_error_if_reference_is_not_a_valid_uu
status='000', provider_reference='something-bad', client_name='sms-client')
@pytest.mark.parametrize('client_name', ('Firetext', 'MMG'))
@pytest.mark.parametrize('client_name', ('Firetext', 'MMG', 'Reach'))
def test_process_sms_response_raises_client_exception_for_unknown_status(
sample_notification,
mocker,
@@ -43,6 +43,9 @@ def test_process_sms_response_raises_client_exception_for_unknown_status(
('3', '2', 'MMG', 'delivered', "Delivered to operator"),
('4', '27', 'MMG', 'temporary-failure', "Absent Subscriber"),
('5', '13', 'MMG', 'permanent-failure', "Sender id blacklisted"),
('TODO-d', None, 'Reach', 'delivered', "TODO: Delivered"),
('TODO-tf', None, 'Reach', 'temporary-failure', "TODO: Temporary failure"),
('TODO-pf', None, 'Reach', 'permanent-failure', "TODO: Permanent failure"),
])
def test_process_sms_client_response_updates_notification_status(
sample_notification,

View File

@@ -0,0 +1 @@
# TODO: all of the tests

View File

@@ -0,0 +1,99 @@
import pytest
from flask import json
def dvla_post(client, data):
return client.post(
path='/notifications/letter/dvla',
data=data,
headers=[('Content-Type', 'application/json')]
)
def test_dvla_callback_returns_400_with_invalid_request(client):
data = json.dumps({"foo": "bar"})
response = dvla_post(client, data)
assert response.status_code == 400
def test_dvla_callback_autoconfirms_subscription(client, mocker):
autoconfirm_mock = mocker.patch('app.notifications.notifications_letter_callback.autoconfirm_subscription')
data = _sns_confirmation_callback()
response = dvla_post(client, data)
assert response.status_code == 200
assert autoconfirm_mock.called
def test_dvla_callback_autoconfirm_does_not_call_update_letter_notifications_task(client, mocker):
autoconfirm_mock = mocker.patch('app.notifications.notifications_letter_callback.autoconfirm_subscription')
update_task = \
mocker.patch('app.notifications.notifications_letter_callback.update_letter_notifications_statuses.apply_async')
data = _sns_confirmation_callback()
response = dvla_post(client, data)
assert response.status_code == 200
assert autoconfirm_mock.called
assert not update_task.called
def test_dvla_callback_calls_does_not_update_letter_notifications_task_with_invalid_file_type(client, mocker):
update_task = \
mocker.patch('app.notifications.notifications_letter_callback.update_letter_notifications_statuses.apply_async')
data = _sample_sns_s3_callback("bar.txt")
response = dvla_post(client, data)
assert response.status_code == 200
assert not update_task.called
@pytest.mark.parametrize("filename",
['Notify-20170411153023-rs.txt', 'Notify-20170411153023-rsp.txt'])
def test_dvla_rs_and_rsp_txt_file_callback_calls_update_letter_notifications_task(client, mocker, filename):
update_task = mocker.patch(
'app.notifications.notifications_letter_callback.update_letter_notifications_statuses.apply_async')
daily_sorted_counts_task = mocker.patch(
'app.notifications.notifications_letter_callback.record_daily_sorted_counts.apply_async')
data = _sample_sns_s3_callback(filename)
response = dvla_post(client, data)
assert response.status_code == 200
assert update_task.called
update_task.assert_called_with([filename], queue='notify-internal-tasks')
daily_sorted_counts_task.assert_called_with([filename], queue='notify-internal-tasks')
def test_dvla_ack_calls_does_not_call_letter_notifications_task(client, mocker):
update_task = mocker.patch(
'app.notifications.notifications_letter_callback.update_letter_notifications_statuses.apply_async')
daily_sorted_counts_task = mocker.patch(
'app.notifications.notifications_letter_callback.record_daily_sorted_counts.apply_async')
data = _sample_sns_s3_callback('bar.ack.txt')
response = dvla_post(client, data)
assert response.status_code == 200
update_task.assert_not_called()
daily_sorted_counts_task.assert_not_called()
def _sample_sns_s3_callback(filename):
message_contents = '''{"Records":[{"eventVersion":"2.0","eventSource":"aws:s3","awsRegion":"eu-west-1","eventTime":"2017-05-16T11:38:41.073Z","eventName":"ObjectCreated:Put","userIdentity":{"principalId":"some-p-id"},"requestParameters":{"sourceIPAddress":"8.8.8.8"},"responseElements":{"x-amz-request-id":"some-r-id","x-amz-id-2":"some-x-am-id"},"s3":{"s3SchemaVersion":"1.0","configurationId":"some-c-id","bucket":{"name":"some-bucket","ownerIdentity":{"principalId":"some-p-id"},"arn":"some-bucket-arn"},
"object":{"key":"%s"}}}]}''' % (filename) # noqa
return json.dumps({
"SigningCertURL": "foo.pem",
"UnsubscribeURL": "bar",
"Signature": "some-signature",
"Type": "Notification",
"Timestamp": "2016-05-03T08:35:12.884Z",
"SignatureVersion": "1",
"MessageId": "6adbfe0a-d610-509a-9c47-af894e90d32d",
"Subject": "Amazon S3 Notification",
"TopicArn": "sample-topic-arn",
"Message": message_contents
})
def _sns_confirmation_callback():
return b'{\n "Type": "SubscriptionConfirmation",\n "MessageId": "165545c9-2a5c-472c-8df2-7ff2be2b3b1b",\n "Token": "2336412f37fb687f5d51e6e241d09c805a5a57b30d712f794cc5f6a988666d92768dd60a747ba6f3beb71854e285d6ad02428b09ceece29417f1f02d609c582afbacc99c583a916b9981dd2728f4ae6fdb82efd087cc3b7849e05798d2d2785c03b0879594eeac82c01f235d0e717736",\n "TopicArn": "arn:aws:sns:us-west-2:123456789012:MyTopic",\n "Message": "You have chosen to subscribe to the topic arn:aws:sns:us-west-2:123456789012:MyTopic.\\nTo confirm the subscription, visit the SubscribeURL included in this message.",\n "SubscribeURL": "https://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:123456789012:MyTopic&Token=2336412f37fb687f5d51e6e241d09c805a5a57b30d712f794cc5f6a988666d92768dd60a747ba6f3beb71854e285d6ad02428b09ceece29417f1f02d609c582afbacc99c583a916b9981dd2728f4ae6fdb82efd087cc3b7849e05798d2d2785c03b0879594eeac82c01f235d0e717736",\n "Timestamp": "2012-04-26T20:45:04.751Z",\n "SignatureVersion": "1",\n "Signature": "EXAMPLEpH+DcEwjAPg8O9mY8dReBSwksfg2S7WKQcikcNKWLQjwu6A4VbeS0QHVCkhRS7fUQvi2egU3N858fiTDN6bkkOxYDVrY0Ad8L10Hs3zH81mtnPk5uvvolIC1CXGu43obcgFxeL3khZl8IKvO61GWB6jI9b5+gLPoBc1Q=",\n "SigningCertURL": "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem"\n}' # noqa

View File

@@ -1,4 +1,3 @@
import pytest
from flask import json
from app.notifications.notifications_sms_callback import validate_callback_data
@@ -8,96 +7,21 @@ def firetext_post(client, data):
return client.post(
path='/notifications/sms/firetext',
data=data,
headers=[
('Content-Type', 'application/x-www-form-urlencoded'),
('X-Forwarded-For', '203.0.113.195, 70.41.3.18, 150.172.238.178') # fake IPs
])
headers=[('Content-Type', 'application/x-www-form-urlencoded')])
def mmg_post(client, data):
return client.post(
path='/notifications/sms/mmg',
data=data,
headers=[
('Content-Type', 'application/json'),
('X-Forwarded-For', '203.0.113.195, 70.41.3.18, 150.172.238.178') # fake IPs
])
headers=[('Content-Type', 'application/json')])
def dvla_post(client, data):
def reach_post(client, data):
return client.post(
path='/notifications/letter/dvla',
path='/notifications/sms/reach',
data=data,
headers=[('Content-Type', 'application/json')]
)
def test_dvla_callback_returns_400_with_invalid_request(client):
data = json.dumps({"foo": "bar"})
response = dvla_post(client, data)
assert response.status_code == 400
def test_dvla_callback_autoconfirms_subscription(client, mocker):
autoconfirm_mock = mocker.patch('app.notifications.notifications_letter_callback.autoconfirm_subscription')
data = _sns_confirmation_callback()
response = dvla_post(client, data)
assert response.status_code == 200
assert autoconfirm_mock.called
def test_dvla_callback_autoconfirm_does_not_call_update_letter_notifications_task(client, mocker):
autoconfirm_mock = mocker.patch('app.notifications.notifications_letter_callback.autoconfirm_subscription')
update_task = \
mocker.patch('app.notifications.notifications_letter_callback.update_letter_notifications_statuses.apply_async')
data = _sns_confirmation_callback()
response = dvla_post(client, data)
assert response.status_code == 200
assert autoconfirm_mock.called
assert not update_task.called
def test_dvla_callback_calls_does_not_update_letter_notifications_task_with_invalid_file_type(client, mocker):
update_task = \
mocker.patch('app.notifications.notifications_letter_callback.update_letter_notifications_statuses.apply_async')
data = _sample_sns_s3_callback("bar.txt")
response = dvla_post(client, data)
assert response.status_code == 200
assert not update_task.called
@pytest.mark.parametrize("filename",
['Notify-20170411153023-rs.txt', 'Notify-20170411153023-rsp.txt'])
def test_dvla_rs_and_rsp_txt_file_callback_calls_update_letter_notifications_task(client, mocker, filename):
update_task = mocker.patch(
'app.notifications.notifications_letter_callback.update_letter_notifications_statuses.apply_async')
daily_sorted_counts_task = mocker.patch(
'app.notifications.notifications_letter_callback.record_daily_sorted_counts.apply_async')
data = _sample_sns_s3_callback(filename)
response = dvla_post(client, data)
assert response.status_code == 200
assert update_task.called
update_task.assert_called_with([filename], queue='notify-internal-tasks')
daily_sorted_counts_task.assert_called_with([filename], queue='notify-internal-tasks')
def test_dvla_ack_calls_does_not_call_letter_notifications_task(client, mocker):
update_task = mocker.patch(
'app.notifications.notifications_letter_callback.update_letter_notifications_statuses.apply_async')
daily_sorted_counts_task = mocker.patch(
'app.notifications.notifications_letter_callback.record_daily_sorted_counts.apply_async')
data = _sample_sns_s3_callback('bar.ack.txt')
response = dvla_post(client, data)
assert response.status_code == 200
update_task.assert_not_called()
daily_sorted_counts_task.assert_not_called()
headers=[('Content-Type', 'application/json')])
def test_firetext_callback_should_not_need_auth(client, mocker):
@@ -218,6 +142,24 @@ def test_mmg_callback_should_return_200_and_call_task_with_valid_data(client, mo
)
# TODO: more tests about edge cases for this provider
def test_reach_callback_should_return_200_and_call_task_with_valid_data(client, mocker):
mock_celery = mocker.patch(
'app.notifications.notifications_sms_callback.process_sms_client_response.apply_async')
data = json.dumps({"data": "TODO"})
response = reach_post(client, data)
assert response.status_code == 200
json_data = json.loads(response.data)
assert json_data['result'] == 'success'
mock_celery.assert_called_once_with(
['TODO-d', 'notification_id', 'Reach', 'something'],
queue='sms-callbacks',
)
def test_validate_callback_data_returns_none_when_valid():
form = {'status': 'good',
'reference': 'send-sms-code'}
@@ -255,24 +197,3 @@ def test_validate_callback_data_returns_error_for_empty_string():
result = validate_callback_data(form, fields, client_name)
assert result is not None
assert "{} callback failed: {} missing".format(client_name, 'status') in result
def _sample_sns_s3_callback(filename):
message_contents = '''{"Records":[{"eventVersion":"2.0","eventSource":"aws:s3","awsRegion":"eu-west-1","eventTime":"2017-05-16T11:38:41.073Z","eventName":"ObjectCreated:Put","userIdentity":{"principalId":"some-p-id"},"requestParameters":{"sourceIPAddress":"8.8.8.8"},"responseElements":{"x-amz-request-id":"some-r-id","x-amz-id-2":"some-x-am-id"},"s3":{"s3SchemaVersion":"1.0","configurationId":"some-c-id","bucket":{"name":"some-bucket","ownerIdentity":{"principalId":"some-p-id"},"arn":"some-bucket-arn"},
"object":{"key":"%s"}}}]}''' % (filename) # noqa
return json.dumps({
"SigningCertURL": "foo.pem",
"UnsubscribeURL": "bar",
"Signature": "some-signature",
"Type": "Notification",
"Timestamp": "2016-05-03T08:35:12.884Z",
"SignatureVersion": "1",
"MessageId": "6adbfe0a-d610-509a-9c47-af894e90d32d",
"Subject": "Amazon S3 Notification",
"TopicArn": "sample-topic-arn",
"Message": message_contents
})
def _sns_confirmation_callback():
return b'{\n "Type": "SubscriptionConfirmation",\n "MessageId": "165545c9-2a5c-472c-8df2-7ff2be2b3b1b",\n "Token": "2336412f37fb687f5d51e6e241d09c805a5a57b30d712f794cc5f6a988666d92768dd60a747ba6f3beb71854e285d6ad02428b09ceece29417f1f02d609c582afbacc99c583a916b9981dd2728f4ae6fdb82efd087cc3b7849e05798d2d2785c03b0879594eeac82c01f235d0e717736",\n "TopicArn": "arn:aws:sns:us-west-2:123456789012:MyTopic",\n "Message": "You have chosen to subscribe to the topic arn:aws:sns:us-west-2:123456789012:MyTopic.\\nTo confirm the subscription, visit the SubscribeURL included in this message.",\n "SubscribeURL": "https://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:123456789012:MyTopic&Token=2336412f37fb687f5d51e6e241d09c805a5a57b30d712f794cc5f6a988666d92768dd60a747ba6f3beb71854e285d6ad02428b09ceece29417f1f02d609c582afbacc99c583a916b9981dd2728f4ae6fdb82efd087cc3b7849e05798d2d2785c03b0879594eeac82c01f235d0e717736",\n "Timestamp": "2012-04-26T20:45:04.751Z",\n "SignatureVersion": "1",\n "Signature": "EXAMPLEpH+DcEwjAPg8O9mY8dReBSwksfg2S7WKQcikcNKWLQjwu6A4VbeS0QHVCkhRS7fUQvi2egU3N858fiTDN6bkkOxYDVrY0Ad8L10Hs3zH81mtnPk5uvvolIC1CXGu43obcgFxeL3khZl8IKvO61GWB6jI9b5+gLPoBc1Q=",\n "SigningCertURL": "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem"\n}' # noqa