diff --git a/app/celery/process_sms_client_response_tasks.py b/app/celery/process_sms_client_response_tasks.py index e16175646..b8f1f63bb 100644 --- a/app/celery/process_sms_client_response_tasks.py +++ b/app/celery/process_sms_client_response_tasks.py @@ -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 } diff --git a/app/clients/sms/reach.py b/app/clients/sms/reach.py new file mode 100644 index 000000000..d9b016e2a --- /dev/null +++ b/app/clients/sms/reach.py @@ -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 diff --git a/app/notifications/notifications_sms_callback.py b/app/notifications/notifications_sms_callback.py index e0111ea5a..f7353d45c 100644 --- a/app/notifications/notifications_sms_callback.py +++ b/app/notifications/notifications_sms_callback.py @@ -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, diff --git a/manifest.yml.j2 b/manifest.yml.j2 index 3560f7b32..dbd267fd3 100644 --- a/manifest.yml.j2 +++ b/manifest.yml.j2 @@ -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, diff --git a/tests/app/notifications/test_process_client_response.py b/tests/app/celery/test_process_sms_client_response_tasks.py similarity index 95% rename from tests/app/notifications/test_process_client_response.py rename to tests/app/celery/test_process_sms_client_response_tasks.py index 6005fbce3..046566906 100644 --- a/tests/app/notifications/test_process_client_response.py +++ b/tests/app/celery/test_process_sms_client_response_tasks.py @@ -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, diff --git a/tests/app/clients/test_reach.py b/tests/app/clients/test_reach.py new file mode 100644 index 000000000..00cf4f034 --- /dev/null +++ b/tests/app/clients/test_reach.py @@ -0,0 +1 @@ +# TODO: all of the tests diff --git a/tests/app/notifications/test_notifications_letter_callbacks.py b/tests/app/notifications/test_notifications_letter_callbacks.py new file mode 100644 index 000000000..c0d1a1321 --- /dev/null +++ b/tests/app/notifications/test_notifications_letter_callbacks.py @@ -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 diff --git a/tests/app/notifications/rest/test_callbacks.py b/tests/app/notifications/test_notifications_sms_callbacks.py similarity index 53% rename from tests/app/notifications/rest/test_callbacks.py rename to tests/app/notifications/test_notifications_sms_callbacks.py index 2b4dc2a60..1c1a02f01 100644 --- a/tests/app/notifications/rest/test_callbacks.py +++ b/tests/app/notifications/test_notifications_sms_callbacks.py @@ -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