diff --git a/app/__init__.py b/app/__init__.py index bfd519fe5..456194694 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -150,9 +150,6 @@ def register_blueprint(application): from app.notifications.notifications_ses_callback import ( ses_callback_blueprint, ) - from app.notifications.notifications_sms_callback import ( - sms_callback_blueprint, - ) from app.notifications.receive_notifications import ( receive_notifications_blueprint, ) @@ -201,11 +198,6 @@ def register_blueprint(application): ses_callback_blueprint.before_request(requires_no_auth) application.register_blueprint(ses_callback_blueprint) - # delivery receipts - # TODO: make sure research mode can still trigger sms callbacks, then re-enable this - sms_callback_blueprint.before_request(requires_no_auth) - application.register_blueprint(sms_callback_blueprint) - # inbound sms receive_notifications_blueprint.before_request(requires_no_auth) application.register_blueprint(receive_notifications_blueprint) diff --git a/app/celery/process_sms_client_response_tasks.py b/app/celery/process_sms_client_response_tasks.py deleted file mode 100644 index aff0aad15..000000000 --- a/app/celery/process_sms_client_response_tasks.py +++ /dev/null @@ -1,80 +0,0 @@ -from app import notify_celery - -sms_response_mapper = { - # 'SNS': get_sns_responses, -} - - -# this is used by notifications_sms_callback and needs to be heavily changed for SNS -# leaving for now as an example of what MMG did and what we may want to replicate in the eventual -# SNS method -@notify_celery.task(bind=True, name="process-sms-client-response", max_retries=5, default_retry_delay=300) -def process_sms_client_response(self, status, provider_reference, client_name, detailed_status_code=None): - raise Exception("process_sms_client_response not implemented") -# # validate reference -# try: -# uuid.UUID(provider_reference, version=4) -# except ValueError as e: -# current_app.logger.exception(f'{client_name} callback with invalid reference {provider_reference}') -# raise e - -# response_parser = sms_response_mapper[client_name] - -# # validate status -# try: -# notification_status, detailed_status = response_parser(status, detailed_status_code) -# current_app.logger.info( -# f'{client_name} callback returned status of {notification_status}' -# f'({status}): {detailed_status}({detailed_status_code}) for reference: {provider_reference}' -# ) -# except KeyError: -# _process_for_status( -# notification_status='technical-failure', -# client_name=client_name, -# provider_reference=provider_reference -# ) -# raise ClientException(f'{client_name} callback failed: status {status} not found.') - -# _process_for_status( -# notification_status=notification_status, -# client_name=client_name, -# provider_reference=provider_reference, -# detailed_status_code=detailed_status_code -# ) - - -# def _process_for_status(notification_status, client_name, provider_reference, detailed_status_code=None): -# # record stats -# notification = notifications_dao.update_notification_status_by_id( -# notification_id=provider_reference, -# status=notification_status, -# sent_by=client_name.lower(), -# detailed_status_code=detailed_status_code -# ) -# if not notification: -# return - -# statsd_client.incr('callback.{}.{}'.format(client_name.lower(), notification_status)) - -# if notification.sent_at: -# statsd_client.timing_with_dates( -# f'callback.{client_name.lower()}.{notification_status}.elapsed-time', -# datetime.utcnow(), -# notification.sent_at -# ) - -# if notification.billable_units == 0: -# service = notification.service -# template_model = dao_get_template_by_id(notification.template_id, notification.template_version) - -# template = SMSMessageTemplate( -# template_model.__dict__, -# values=notification.personalisation, -# prefix=service.name, -# show_prefix=service.prefix_sms, -# ) -# notification.billable_units = template.fragment_count -# notifications_dao.dao_update_notification(notification) - -# if notification_status != NOTIFICATION_PENDING: -# check_and_queue_callback_task(notification) diff --git a/app/notifications/notifications_sms_callback.py b/app/notifications/notifications_sms_callback.py deleted file mode 100644 index 87fb99c3d..000000000 --- a/app/notifications/notifications_sms_callback.py +++ /dev/null @@ -1,42 +0,0 @@ -from flask import Blueprint - -from app.errors import register_errors - -sms_callback_blueprint = Blueprint("sms_callback", __name__, url_prefix="/notifications/sms") -register_errors(sms_callback_blueprint) - -# TODO SNS SMS delivery receipts delivered here -# This file should likely be deleted, since SNS does not use callback https calls -# Leaving for now to have an example of what jobs MMG did that we may want to replicate in the -# eventual SNS method. - -# @sms_callback_blueprint.route('/mmg', methods=['POST']) -# def process_mmg_response(): -# client_name = 'MMG' -# data = json.loads(request.data) -# errors = validate_callback_data(data=data, -# fields=['status', 'CID'], -# client_name=client_name) -# if errors: -# raise InvalidRequest(errors, status_code=400) - -# status = str(data.get('status')) -# detailed_status_code = str(data.get('substatus')) - -# provider_reference = data.get('CID') - -# process_sms_client_response.apply_async( -# [status, provider_reference, client_name, detailed_status_code], -# queue=QueueNames.SMS_CALLBACKS, -# ) - -# return jsonify(result='success'), 200 - - -def validate_callback_data(data, fields, client_name): - errors = [] - for f in fields: - if not str(data.get(f, '')): - error = "{} callback failed: {} missing".format(client_name, f) - errors.append(error) - return errors if len(errors) > 0 else None diff --git a/tests/app/celery/test_process_sms_client_response_tasks.py b/tests/app/celery/test_process_sms_client_response_tasks.py deleted file mode 100644 index 34bd6807c..000000000 --- a/tests/app/celery/test_process_sms_client_response_tasks.py +++ /dev/null @@ -1,179 +0,0 @@ -import uuid -from datetime import datetime - -import pytest -from freezegun import freeze_time - -from app import statsd_client -from app.celery.process_sms_client_response_tasks import ( - process_sms_client_response, -) -from app.clients import ClientException -from app.models import NOTIFICATION_TECHNICAL_FAILURE - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -def test_process_sms_client_response_raises_error_if_reference_is_not_a_valid_uuid(client): - with pytest.raises(ValueError): - process_sms_client_response( - status='000', provider_reference='something-bad', client_name='sms-client') - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -@pytest.mark.parametrize('client_name', ('Firetext', 'MMG')) -def test_process_sms_response_raises_client_exception_for_unknown_status( - sample_notification, - mocker, - client_name, -): - with pytest.raises(ClientException) as e: - process_sms_client_response( - status='000', - provider_reference=str(sample_notification.id), - client_name=client_name, - ) - - assert f"{client_name} callback failed: status {'000'} not found." in str(e.value) - assert sample_notification.status == NOTIFICATION_TECHNICAL_FAILURE - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -@pytest.mark.parametrize('status, detailed_status_code, sms_provider, expected_notification_status, reason', [ - ('0', None, 'Firetext', 'delivered', None), - ('1', '101', 'Firetext', 'permanent-failure', 'Unknown Subscriber'), - ('2', '102', 'Firetext', 'pending', 'Absent Subscriber'), - ('2', '1', 'MMG', 'permanent-failure', "Number does not exist"), - ('3', '2', 'MMG', 'delivered', "Delivered to operator"), - ('4', '27', 'MMG', 'temporary-failure', "Absent Subscriber"), - ('5', '13', 'MMG', 'permanent-failure', "Sender id blacklisted"), -]) -def test_process_sms_client_response_updates_notification_status( - sample_notification, - mocker, - status, - detailed_status_code, - sms_provider, - expected_notification_status, - reason -): - mock_logger = mocker.patch('app.celery.tasks.current_app.logger.info') - sample_notification.status = 'sending' - - process_sms_client_response(status, str(sample_notification.id), sms_provider, detailed_status_code) - - message = f"{sms_provider} callback returned status of {expected_notification_status}({status}): {reason}({detailed_status_code}) for reference: {sample_notification.id}" # noqa - mock_logger.assert_any_call(message) - assert sample_notification.status == expected_notification_status - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -@pytest.mark.parametrize('detailed_status_code, expected_notification_status, reason', [ - ('101', 'permanent-failure', 'Unknown Subscriber'), - ('102', 'temporary-failure', 'Absent Subscriber'), - (None, 'temporary-failure', None), - ('000', 'temporary-failure', 'No error reported') -]) -def test_process_sms_client_response_updates_notification_status_when_called_second_time( - sample_notification, - mocker, - detailed_status_code, - expected_notification_status, - reason -): - mock_logger = mocker.patch('app.celery.tasks.current_app.logger.info') - sample_notification.status = 'sending' - process_sms_client_response('2', str(sample_notification.id), 'Firetext') - - process_sms_client_response('1', str(sample_notification.id), 'Firetext', detailed_status_code) - - if detailed_status_code: - message = f'Updating notification id {sample_notification.id} to status {expected_notification_status}, reason: {reason}' # noqa - mock_logger.assert_called_with(message) - - assert sample_notification.status == expected_notification_status - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -@pytest.mark.parametrize('detailed_status_code', ['102', None, '000']) -def test_process_sms_client_response_updates_notification_status_to_pending_with_and_without_failure_code_present( - sample_notification, - mocker, - detailed_status_code -): - sample_notification.status = 'sending' - - process_sms_client_response('2', str(sample_notification.id), 'Firetext', detailed_status_code) - - assert sample_notification.status == 'pending' - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -def test_process_sms_client_response_updates_notification_status_when_detailed_status_code_not_recognised( - sample_notification, - mocker, -): - mock_logger = mocker.patch('app.celery.tasks.current_app.logger.warning') - sample_notification.status = 'sending' - process_sms_client_response('2', str(sample_notification.id), 'Firetext') - - process_sms_client_response('1', str(sample_notification.id), 'Firetext', '789') - - mock_logger.assert_called_once_with('Failure code 789 from Firetext not recognised') - assert sample_notification.status == 'temporary-failure' - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -def test_sms_response_does_not_send_callback_if_notification_is_not_in_the_db(sample_service, mocker): - send_mock = mocker.patch('app.celery.process_sms_client_response_tasks.check_and_queue_callback_task') - reference = str(uuid.uuid4()) - process_sms_client_response(status='3', provider_reference=reference, client_name='MMG') - send_mock.assert_not_called() - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -@freeze_time('2001-01-01T12:00:00') -def test_process_sms_client_response_records_statsd_metrics(sample_notification, client, mocker): - mocker.patch('app.statsd_client.incr') - mocker.patch('app.statsd_client.timing_with_dates') - - sample_notification.status = 'sending' - sample_notification.sent_at = datetime.utcnow() - - process_sms_client_response('0', str(sample_notification.id), 'Firetext') - - statsd_client.incr.assert_any_call("callback.firetext.delivered") - statsd_client.timing_with_dates.assert_any_call( - "callback.firetext.delivered.elapsed-time", datetime.utcnow(), sample_notification.sent_at - ) - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -def test_process_sms_updates_billable_units_if_zero(sample_notification): - sample_notification.billable_units = 0 - process_sms_client_response('3', str(sample_notification.id), 'MMG') - - assert sample_notification.billable_units == 1 - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -def test_process_sms_response_does_not_send_service_callback_for_pending_notifications(sample_notification, mocker): - send_mock = mocker.patch('app.celery.process_sms_client_response_tasks.check_and_queue_callback_task') - process_sms_client_response('2', str(sample_notification.id), 'Firetext') - send_mock.assert_not_called() - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -def test_outcome_statistics_called_for_successful_callback(sample_notification, mocker): - send_mock = mocker.patch('app.celery.process_sms_client_response_tasks.check_and_queue_callback_task') - reference = str(sample_notification.id) - - process_sms_client_response('3', reference, 'MMG') - send_mock.assert_called_once_with(sample_notification) - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -def test_process_sms_updates_sent_by_with_client_name_if_not_in_noti(sample_notification): - sample_notification.sent_by = None - process_sms_client_response('3', str(sample_notification.id), 'MMG') - - assert sample_notification.sent_by == 'mmg' diff --git a/tests/app/notifications/test_notifications_sms_callbacks.py b/tests/app/notifications/test_notifications_sms_callbacks.py deleted file mode 100644 index 773aba892..000000000 --- a/tests/app/notifications/test_notifications_sms_callbacks.py +++ /dev/null @@ -1,103 +0,0 @@ -import pytest -from flask import json - -from app.notifications.notifications_sms_callback import validate_callback_data - - -def mmg_post(client, data): - return client.post( - path='/notifications/sms/mmg', - data=data, - headers=[('Content-Type', 'application/json')]) - - -@pytest.mark.skip(reason="Needs updating for TTS: MMG removal") -def test_mmg_callback_should_not_need_auth(client, mocker, sample_notification): - mocker.patch('app.notifications.notifications_sms_callback.process_sms_client_response') - data = json.dumps({"reference": "mmg_reference", - "CID": str(sample_notification.id), - "MSISDN": "447777349060", - "status": "3", - "deliverytime": "2016-04-05 16:01:07"}) - - response = mmg_post(client, data) - assert response.status_code == 200 - - -@pytest.mark.skip(reason="Needs updating for TTS: MMG removal") -def test_process_mmg_response_returns_400_for_malformed_data(client): - data = json.dumps({"reference": "mmg_reference", - "monkey": 'random thing', - "MSISDN": "447777349060", - "no_status": 00, - "deliverytime": "2016-04-05 16:01:07"}) - - response = mmg_post(client, data) - assert response.status_code == 400 - json_data = json.loads(response.data) - assert json_data['result'] == 'error' - assert len(json_data['message']) == 2 - assert "{} callback failed: {} missing".format('MMG', 'status') in json_data['message'] - assert "{} callback failed: {} missing".format('MMG', 'CID') in json_data['message'] - - -@pytest.mark.skip(reason="Needs updating for TTS: MMG removal") -def test_mmg_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({"reference": "mmg_reference", - "CID": "notification_id", - "MSISDN": "447777349060", - "status": "3", - "substatus": "5", - "deliverytime": "2016-04-05 16:01:07"}) - - response = mmg_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( - ['3', 'notification_id', 'MMG', '5'], - queue='sms-callbacks', - ) - - -def test_validate_callback_data_returns_none_when_valid(): - form = {'status': 'good', - 'reference': 'send-sms-code'} - fields = ['status', 'reference'] - client_name = 'sms client' - - assert validate_callback_data(form, fields, client_name) is None - - -def test_validate_callback_data_return_errors_when_fields_are_empty(): - form = {'monkey': 'good'} - fields = ['status', 'cid'] - client_name = 'sms client' - - errors = validate_callback_data(form, fields, client_name) - assert len(errors) == 2 - assert "{} callback failed: {} missing".format(client_name, 'status') in errors - assert "{} callback failed: {} missing".format(client_name, 'cid') in errors - - -def test_validate_callback_data_can_handle_integers(): - form = {'status': 00, 'cid': 'fsdfadfsdfas'} - fields = ['status', 'cid'] - client_name = 'sms client' - - result = validate_callback_data(form, fields, client_name) - assert result is None - - -def test_validate_callback_data_returns_error_for_empty_string(): - form = {'status': '', 'cid': 'fsdfadfsdfas'} - fields = ['status', 'cid'] - client_name = 'sms client' - - result = validate_callback_data(form, fields, client_name) - assert result is not None - assert "{} callback failed: {} missing".format(client_name, 'status') in result