From e69c34f92f3635fd0bec1d93443278410c67a7f6 Mon Sep 17 00:00:00 2001 From: Kyle MacMillan <16893311+k-macmillan@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:17:28 -0500 Subject: [PATCH] #2104 - Update Twilio Error Codes (#2147) --- .talismanrc | 2 + .../process_delivery_status_result_tasks.py | 2 +- app/clients/sms/twilio.py | 48 +++--- app/constants.py | 5 + .../twilio_signature_utils.py | 17 +- tests/app/celery/test_tasks.py | 2 + tests/app/clients/test_twilio.py | 147 ++++-------------- 7 files changed, 81 insertions(+), 142 deletions(-) diff --git a/.talismanrc b/.talismanrc index 35ae2fc75f..22d0909478 100644 --- a/.talismanrc +++ b/.talismanrc @@ -83,4 +83,6 @@ fileignoreconfig: checksum: 4f0f4d7a4113762219e45a51f7b26a7c0cb83f1d8f10c5598533f6cdcf0e0ada - filename: tests/lambda_functions/vetext_incoming_forwarder_lambda/test_vetext_incoming_forwarder_lambda.py checksum: 7494eb4321fd2fbc3ff3915d8753d8fec7a936a69dc6ab78f0b532a701f032eb +- filename: tests/app/clients/test_twilio.py + checksum: cad49e634cc5ba56157358aa3dfba2dafe7b9dbd3a0c580ec5cda3072f6a76e5 version: "1.0" diff --git a/app/celery/process_delivery_status_result_tasks.py b/app/celery/process_delivery_status_result_tasks.py index 942377e8e7..e902ea0b1d 100644 --- a/app/celery/process_delivery_status_result_tasks.py +++ b/app/celery/process_delivery_status_result_tasks.py @@ -208,7 +208,7 @@ def sms_status_update( last_updated_at = notification.updated_at current_app.logger.info( - 'Iniital %s logic | reference: %s | notification_id: %s | status: %s | status_reason: %s', + 'Initial %s logic | reference: %s | notification_id: %s | status: %s | status_reason: %s', sms_status.provider, sms_status.reference, notification.id, diff --git a/app/clients/sms/twilio.py b/app/clients/sms/twilio.py index 5395d03035..9e82868501 100644 --- a/app/clients/sms/twilio.py +++ b/app/clients/sms/twilio.py @@ -10,7 +10,7 @@ from twilio.base.exceptions import TwilioRestException from app.celery.exceptions import NonRetryableException -from app.clients.sms import SmsClient, SmsStatusRecord, UNABLE_TO_TRANSLATE +from app.clients.sms import SmsClient, SmsStatusRecord, OPT_OUT_MESSAGE, UNABLE_TO_TRANSLATE from app.constants import ( NOTIFICATION_CREATED, NOTIFICATION_DELIVERED, @@ -18,6 +18,8 @@ NOTIFICATION_SENDING, NOTIFICATION_SENT, NOTIFICATION_TECHNICAL_FAILURE, + NOTIFICATION_TEMPORARY_FAILURE, + RETRYABLE_STATUS_REASON, TWILIO_PROVIDER, ) from app.exceptions import InvalidProviderException @@ -50,6 +52,29 @@ def get_twilio_responses(status): class TwilioSMSClient(SmsClient): RAW_DLR_DONE_DATE_FMT = '%y%m%d%H%M' + twilio_error_code_map = { + '21268': TwilioStatus(21268, NOTIFICATION_PERMANENT_FAILURE, 'Premium numbers are not permitted'), + '21408': TwilioStatus(21408, NOTIFICATION_PERMANENT_FAILURE, 'Invalid region specified'), + '21610': TwilioStatus(21610, NOTIFICATION_PERMANENT_FAILURE, OPT_OUT_MESSAGE), + '21612': TwilioStatus(21612, NOTIFICATION_PERMANENT_FAILURE, 'Invalid to/from combo'), + '21614': TwilioStatus(21614, NOTIFICATION_PERMANENT_FAILURE, 'Non-mobile number'), + '21635': TwilioStatus(21635, NOTIFICATION_PERMANENT_FAILURE, 'Non-mobile number'), + '30001': TwilioStatus(30001, NOTIFICATION_TEMPORARY_FAILURE, 'Queue overflow'), + '30002': TwilioStatus(30002, NOTIFICATION_PERMANENT_FAILURE, 'Account suspended'), + '30003': TwilioStatus(30003, NOTIFICATION_PERMANENT_FAILURE, 'Unreachable destination handset'), + '30004': TwilioStatus(30004, NOTIFICATION_PERMANENT_FAILURE, 'Message blocked'), + '30005': TwilioStatus(30005, NOTIFICATION_PERMANENT_FAILURE, 'Unknown destination handset'), + '30006': TwilioStatus(30006, NOTIFICATION_PERMANENT_FAILURE, 'Landline or unreachable carrier'), + '30007': TwilioStatus(30007, NOTIFICATION_PERMANENT_FAILURE, 'Message filtered'), + '30008': TwilioStatus(30008, NOTIFICATION_TECHNICAL_FAILURE, 'Unknown error'), + '30009': TwilioStatus(30009, NOTIFICATION_TECHNICAL_FAILURE, 'Missing inbound segment'), + '30010': TwilioStatus(30010, NOTIFICATION_TECHNICAL_FAILURE, 'Message price exceeds max price'), + '30024': TwilioStatus(30024, NOTIFICATION_TECHNICAL_FAILURE, 'Sender not provisioned by carrier'), + '30034': TwilioStatus(30034, NOTIFICATION_PERMANENT_FAILURE, 'Used an unregistered 10DLC Number'), + '30500': TwilioStatus(30500, NOTIFICATION_TEMPORARY_FAILURE, RETRYABLE_STATUS_REASON), + '60005': TwilioStatus(60005, NOTIFICATION_TEMPORARY_FAILURE, 'Carrier error'), + } + def __init__( self, account_sid=None, @@ -71,20 +96,6 @@ def __init__( self._auth_token = auth_token self._client = Client(account_sid, auth_token) - self.twilio_error_code_map = { - '30001': TwilioStatus(30001, NOTIFICATION_TECHNICAL_FAILURE, 'Queue overflow'), - '30002': TwilioStatus(30002, NOTIFICATION_PERMANENT_FAILURE, 'Account suspended'), - '30003': TwilioStatus(30003, NOTIFICATION_PERMANENT_FAILURE, 'Unreachable destination handset'), - '30004': TwilioStatus(30004, NOTIFICATION_PERMANENT_FAILURE, 'Message blocked'), - '30005': TwilioStatus(30005, NOTIFICATION_PERMANENT_FAILURE, 'Unknown destination handset'), - '30006': TwilioStatus(30006, NOTIFICATION_PERMANENT_FAILURE, 'Landline or unreachable carrier'), - '30007': TwilioStatus(30007, NOTIFICATION_PERMANENT_FAILURE, 'Message filtered'), - '30008': TwilioStatus(30008, NOTIFICATION_TECHNICAL_FAILURE, 'Unknown error'), - '30009': TwilioStatus(30009, NOTIFICATION_TECHNICAL_FAILURE, 'Missing inbound segment'), - '30010': TwilioStatus(30010, NOTIFICATION_TECHNICAL_FAILURE, 'Message price exceeds max price'), - '30034': TwilioStatus(30034, NOTIFICATION_PERMANENT_FAILURE, 'Used an unregistered 10DLC Number'), - } - self.twilio_notify_status_map = { 'accepted': TwilioStatus(None, NOTIFICATION_SENDING, None), 'scheduled': TwilioStatus(None, NOTIFICATION_SENDING, None), @@ -378,12 +389,13 @@ def _evaluate_status(self, message_sid: str, twilio_delivery_status: str, error_ ) notify_delivery_status: TwilioStatus = self.twilio_notify_status_map[twilio_delivery_status] else: - # Logic not being changed, just want to log this for now + # Error codes may be retained for new messages, meaning a "sending" could retain a 30005 if error_codes: - self.logger.warning( - 'Error code: %s existed but status for message: %s was not failed nor undelivered', + self.logger.info( + 'Error code: %s existed but status for message: %s with status: %s', error_codes[0], message_sid, + twilio_delivery_status, ) notify_delivery_status: TwilioStatus = self.twilio_notify_status_map[twilio_delivery_status] diff --git a/app/constants.py b/app/constants.py index 5c355bfb87..ecae400cfd 100644 --- a/app/constants.py +++ b/app/constants.py @@ -254,3 +254,8 @@ JOB_STATUS_SENT_TO_DVLA, JOB_STATUS_ERROR, ) + +# Status reasons +RETRYABLE_STATUS_REASON = ( + 'Retryable - Notification is unable to be processed at this time. Replay the request to VA Notify.' +) diff --git a/lambda_functions/vetext_incoming_forwarder_lambda/twilio_signature_utils.py b/lambda_functions/vetext_incoming_forwarder_lambda/twilio_signature_utils.py index c1b2d41761..e5591b66d2 100644 --- a/lambda_functions/vetext_incoming_forwarder_lambda/twilio_signature_utils.py +++ b/lambda_functions/vetext_incoming_forwarder_lambda/twilio_signature_utils.py @@ -47,7 +47,7 @@ def validate_signature_and_body(token, uri, body, signature): encoded = urlencode(params).encode() # Turn byte string into a base64 encoded message - msg = base64.b64encode(encoded).decode('utf-8') + msg = base64.b64encode(encoded).decode('utf-8') # noqa: F841 - Util helper. Keeping for understanding assert signature == new_signature @@ -60,18 +60,19 @@ def generate_twilio_signature_and_body( addons: str = '', api_version: str = '2010-04-01', body: str = '', + error_code: str = '', from_number: str = '+18888888888', from_city: str = 'LOS ANGELES', from_country: str = 'US', from_state: str = 'CA', from_zip: str = '12345', message_sid: str = '', + message_status: str = 'received', message_service_sid: str = '', num_media: str = '0', num_segments: str = '1', sms_message_sid: str = '', sms_sid: str = '', - sms_status: str = 'received', to_number: str = '+12345678901', to_city: str = 'PROVIDENCE', to_country: str = 'US', @@ -89,18 +90,20 @@ def generate_twilio_signature_and_body( 'AddOns': addons or '{"status":"successful","message":null,"code":null,"results":{}}', 'ApiVersion': api_version, 'Body': body or f'test body {uuid4()}', + 'ErrorCode': error_code, 'From': from_number, 'FromCity': from_city, 'FromCountry': from_country, 'FromState': from_state, 'FromZip': from_zip, 'MessageSid': message_sid or msg_sid, + 'MessageStatus': message_status, 'MessagingServiceSid': message_service_sid or f'MG{uuid4()}'.replace('-', ''), 'NumMedia': num_media, 'NumSegments': num_segments, 'SmsMessageSid': sms_message_sid or msg_sid, 'SmsSid': sms_sid or msg_sid, - 'SmsStatus': sms_status, + 'SmsStatus': message_status, # Appears deprecated, used in incoming forwarder though 'To': to_number, 'ToCity': to_city, 'ToCountry': to_country, @@ -122,12 +125,12 @@ def generate_twilio_signature_and_body( if __name__ == '__main__': # How to generate a test body and signature # To test real events use VEText's token. Ask the Tech Lead or QA. Tokens are not shared with the team. - token = '12345678' - rv = RequestValidator(token) + fake_token = '12345678' # nosec + rv = RequestValidator(fake_token) uri = 'https://staging-api.va.gov/vanotify/twoway/vettext' - signature, body = generate_twilio_signature_and_body(token, uri) + signature, body = generate_twilio_signature_and_body(fake_token, uri) print(f'Body: {body}\n, Signature: {signature}') ###################################### For Understanding Each Part of the Process ###################################### @@ -148,4 +151,4 @@ def generate_twilio_signature_and_body( new_signature = rv.compute_signature(uri, params) print(new_signature) - validate_signature_and_body(token, uri, body, signature) + validate_signature_and_body(fake_token, uri, body, signature) diff --git a/tests/app/celery/test_tasks.py b/tests/app/celery/test_tasks.py index 2d9897605c..95cb0dedeb 100644 --- a/tests/app/celery/test_tasks.py +++ b/tests/app/celery/test_tasks.py @@ -743,6 +743,7 @@ def test_should_put_save_email_task_in_research_mode_queue_if_research_mode_serv ) +@pytest.mark.serial def test_should_save_sms_template_to_and_persist_with_job_id( notify_db_session, sample_template, @@ -758,6 +759,7 @@ def test_should_save_sms_template_to_and_persist_with_job_id( notification_id = uuid4() now = datetime.utcnow() + # serial - Fails intermittently save_sms( job.service.id, notification_id, diff --git a/tests/app/clients/test_twilio.py b/tests/app/clients/test_twilio.py index 244437e0ba..6544efe60d 100644 --- a/tests/app/clients/test_twilio.py +++ b/tests/app/clients/test_twilio.py @@ -10,7 +10,7 @@ from app import twilio_sms_client from app.celery.exceptions import NonRetryableException from app.clients.sms import SmsStatusRecord -from app.clients.sms.twilio import get_twilio_responses, TwilioSMSClient +from app.clients.sms.twilio import get_twilio_responses, TwilioSMSClient, TwilioStatus from app.constants import ( NOTIFICATION_DELIVERED, NOTIFICATION_TECHNICAL_FAILURE, @@ -19,9 +19,14 @@ NOTIFICATION_SENT, ) from app.exceptions import InvalidProviderException +from lambda_functions.vetext_incoming_forwarder_lambda.twilio_signature_utils import generate_twilio_signature_and_body from tests.app.db import create_service_sms_sender +FAKE_DELIVERY_STATUS_URI = 'https://api.va.gov/sms/deliverystatus' +FAKE_DELIVERY_STATUS_TOKEN = 'unit_test' + + class FakeClient: def __init__(self, **kwargs): self.messages = self.MessageFactory() @@ -173,87 +178,6 @@ def service_sms_sender(request): } -MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30001 = { - 'twilio_status': NOTIFICATION_TECHNICAL_FAILURE, - 'message': 'eyJhcmdzIjogW3siTWVzc2FnZSI6IHsiYm9keSI6ICJSYXdEbHJEb25lRGF0ZT0yMzAzMDkyMDIxJkVycm' - '9yQ29kZT0zMDAwMSZTbXNTaWQ9U014eHgmU21zU3RhdHVzPWZhaWxlZCZNZXNzYWdlU3RhdHVzPWZhaWxlZCZUbz0lMk' - 'IxMTExMTExMTExMSZNZXNzYWdlU2lkPVNNeXl5JkFjY291bnRTaWQ9QUN6enomRnJvbT0lMkIxMjIyMjIyMjIyMiZBcGl' - 'WZXJzaW9uPTIwMTAtMDQtMDEiLCAicHJvdmlkZXIiOiAidHdpbGlvIn19XX0=', -} - - -MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30002 = { - 'twilio_status': NOTIFICATION_PERMANENT_FAILURE, - 'message': 'eyJhcmdzIjogW3siTWVzc2FnZSI6IHsiYm9keSI6ICJSYXdEbHJEb25lRGF0ZT0yMzAzMDkyMDIxJkV' - 'ycm9yQ29kZT0zMDAwMiZTbXNTaWQ9U014eHgmU21zU3RhdHVzPWZhaWxlZCZNZXNzYWdlU3RhdHVzPWZhaWxlZCZU' - 'bz0lMkIxMTExMTExMTExMSZNZXNzYWdlU2lkPVNNeXl5JkFjY291bnRTaWQ9QUN6enomRnJvbT0lMkIxMjIyMjIyM' - 'jIyMiZBcGlWZXJzaW9uPTIwMTAtMDQtMDEiLCAicHJvdmlkZXIiOiAidHdpbGlvIn19XX0=', -} - - -MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30003 = { - 'twilio_status': NOTIFICATION_PERMANENT_FAILURE, - 'message': 'eyJhcmdzIjogW3siTWVzc2FnZSI6IHsiYm9keSI6ICJSYXdEbHJEb25lRGF0ZT0yMzAzMDkyMDIxJ' - 'kVycm9yQ29kZT0zMDAwMyZTbXNTaWQ9U014eHgmU21zU3RhdHVzPWZhaWxlZCZNZXNzYWdlU3RhdHVzPWZhaWxlZ' - 'CZUbz0lMkIxMTExMTExMTExMSZNZXNzYWdlU2lkPVNNeXl5JkFjY291bnRTaWQ9QUN6enomRnJvbT0lMkIxMjIyM' - 'jIyMjIyMiZBcGlWZXJzaW9uPTIwMTAtMDQtMDEiLCAicHJvdmlkZXIiOiAidHdpbGlvIn19XX0=', -} - - -MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30004 = { - 'twilio_status': NOTIFICATION_PERMANENT_FAILURE, - 'message': 'eyJhcmdzIjogW3siTWVzc2FnZSI6IHsiYm9keSI6ICJSYXdEbHJEb25lRGF0ZT0yMzAzMDkyMDIxJkV' - 'ycm9yQ29kZT0zMDAwNCZTbXNTaWQ9U014eHgmU21zU3RhdHVzPWZhaWxlZCZNZXNzYWdlU3RhdHVzPWZhaWxlZCZU' - 'bz0lMkIxMTExMTExMTExMSZNZXNzYWdlU2lkPVNNeXl5JkFjY291bnRTaWQ9QUN6enomRnJvbT0lMkIxMjIyMjIyMj' - 'IyMiZBcGlWZXJzaW9uPTIwMTAtMDQtMDEiLCAicHJvdmlkZXIiOiAidHdpbGlvIn19XX0=', -} - - -MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30005 = { - 'twilio_status': NOTIFICATION_PERMANENT_FAILURE, - 'message': 'eyJhcmdzIjogW3siTWVzc2FnZSI6IHsiYm9keSI6ICJSYXdEbHJEb25lRGF0ZT0yMzAzMDkyMDIxJ' - 'kVycm9yQ29kZT0zMDAwNSZTbXNTaWQ9U014eHgmU21zU3RhdHVzPWZhaWxlZCZNZXNzYWdlU3RhdHVzPWZhaWxlZC' - 'ZUbz0lMkIxMTExMTExMTExMSZNZXNzYWdlU2lkPVNNeXl5JkFjY291bnRTaWQ9QUN6enomRnJvbT0lMkIxMjIyMjI' - 'yMjIyMiZBcGlWZXJzaW9uPTIwMTAtMDQtMDEiLCAicHJvdmlkZXIiOiAidHdpbGlvIn19XX0=', -} - - -MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30006 = { - 'twilio_status': NOTIFICATION_PERMANENT_FAILURE, - 'message': 'eyJhcmdzIjogW3siTWVzc2FnZSI6IHsiYm9keSI6ICJSYXdEbHJEb25lRGF0ZT0yMzAzMDkyMDIx' - 'JkVycm9yQ29kZT0zMDAwNiZTbXNTaWQ9U014eHgmU21zU3RhdHVzPWZhaWxlZCZNZXNzYWdlU3RhdHVzPWZhaWx' - 'lZCZUbz0lMkIxMTExMTExMTExMSZNZXNzYWdlU2lkPVNNeXl5JkFjY291bnRTaWQ9QUN6enomRnJvbT0lMkIxMj' - 'IyMjIyMjIyMiZBcGlWZXJzaW9uPTIwMTAtMDQtMDEiLCAicHJvdmlkZXIiOiAidHdpbGlvIn19XX0=', -} - - -MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30007 = { - 'twilio_status': NOTIFICATION_PERMANENT_FAILURE, - 'message': 'eyJhcmdzIjogW3siTWVzc2FnZSI6IHsiYm9keSI6ICJSYXdEbHJEb25lRGF0ZT0yMzAzMDkyMDIxJk' - 'Vycm9yQ29kZT0zMDAwNyZTbXNTaWQ9U014eHgmU21zU3RhdHVzPWZhaWxlZCZNZXNzYWdlU3RhdHVzPWZhaWxlZCZ' - 'Ubz0lMkIxMTExMTExMTExMSZNZXNzYWdlU2lkPVNNeXl5JkFjY291bnRTaWQ9QUN6enomRnJvbT0lMkIxMjIyMj' - 'IyMjIyMiZBcGlWZXJzaW9uPTIwMTAtMDQtMDEiLCAicHJvdmlkZXIiOiAidHdpbGlvIn19XX0=', -} - - -MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30008 = { - 'twilio_status': NOTIFICATION_TECHNICAL_FAILURE, - 'message': 'eyJhcmdzIjogW3siTWVzc2FnZSI6IHsiYm9keSI6ICJSYXdEbHJEb25lRGF0ZT0yMzAzMDkyMDIxJk' - 'Vycm9yQ29kZT0zMDAwOCZTbXNTaWQ9U014eHgmU21zU3RhdHVzPWZhaWxlZCZNZXNzYWdlU3RhdHVzPWZhaWxlZ' - 'CZUbz0lMkIxMTExMTExMTExMSZNZXNzYWdlU2lkPVNNeXl5JkFjY291bnRTaWQ9QUN6enomRnJvbT0lMkIxMjIyM' - 'jIyMjIyMiZBcGlWZXJzaW9uPTIwMTAtMDQtMDEiLCAicHJvdmlkZXIiOiAidHdpbGlvIn19XX0=', -} - - -MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30009 = { - 'twilio_status': NOTIFICATION_TECHNICAL_FAILURE, - 'message': 'eyJhcmdzIjogW3siTWVzc2FnZSI6IHsiYm9keSI6ICJSYXdEbHJEb25lRGF0ZT0yMzAzMDkyMDIxJk' - 'Vycm9yQ29kZT0zMDAwOSZTbXNTaWQ9U014eHgmU21zU3RhdHVzPWZhaWxlZCZNZXNzYWdlU3RhdHVzPWZhaWxlZCZ' - 'Ubz0lMkIxMTExMTExMTExMSZNZXNzYWdlU2lkPVNNeXl5JkFjY291bnRTaWQ9QUN6enomRnJvbT0lMkIxMjIyMjI' - 'yMjIyMiZBcGlWZXJzaW9uPTIwMTAtMDQtMDEiLCAicHJvdmlkZXIiOiAidHdpbGlvIn19XX0=', -} - - MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30010 = { 'twilio_status': NOTIFICATION_TECHNICAL_FAILURE, 'message': 'eyJhcmdzIjogW3siTWVzc2FnZSI6IHsiYm9keSI6ICJSYXdEbHJEb25lRGF0ZT0yMzAzMDkyMDIx' @@ -263,24 +187,6 @@ def service_sms_sender(request): } -MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30034 = { - 'twilio_status': NOTIFICATION_PERMANENT_FAILURE, - 'message': 'eyJhcmdzIjogW3siTWVzc2FnZSI6IHsiYm9keSI6ICJSYXdEbHJEb25lRGF0ZT0yMzAzMDkyMDIxJk' - 'Vycm9yQ29kZT0zMDAzNCZTbXNTaWQ9U014eHgmU21zU3RhdHVzPWZhaWxlZCZNZXNzYWdlU3RhdHVzPWZhaWxlZCZ' - 'Ubz0lMkIxMTExMTExMTExMSZNZXNzYWdlU2lkPVNNeXl5JkFjY291bnRTaWQ9QUN6enomRnJvbT0lMkIxMjIyMjIy' - 'MjIyMiZBcGlWZXJzaW9uPTIwMTAtMDQtMDEiLCAicHJvdmlkZXIiOiAidHdpbGlvIn19XX0=', -} - - -MESSAAGE_BODY_WITH_FAILED_STATUS_AND_INVALID_ERROR_CODE = { - 'twilio_status': NOTIFICATION_TECHNICAL_FAILURE, - 'message': 'eyJhcmdzIjogW3siTWVzc2FnZSI6IHsiYm9keSI6ICJSYXdEbHJEb25lRGF0ZT0yMzAzMDkyMDIxJ' - 'kVycm9yQ29kZT0zMDAxMSZTbXNTaWQ9U014eHgmU21zU3RhdHVzPWZhaWxlZCZNZXNzYWdlU3RhdHVzPWZhaWxlZ' - 'CZUbz0lMkIxMTExMTExMTExMSZNZXNzYWdlU2lkPVNNeXl5JkFjY291bnRTaWQ9QUN6enomRnJvbT0lMkIxMjIy' - 'MjIyMjIyMiZBcGlWZXJzaW9uPTIwMTAtMDQtMDEiLCAicHJvdmlkZXIiOiAidHdpbGlvIn19XX0=', -} - - MESSAGE_BODY_WITH_NO_MESSAGE_STATUS = { 'twilio_status': None, 'message': 'eyJhcmdzIjogW3siTWVzc2FnZSI6IHsiYm9keSI6ICJSYXdEbHJEb25lRGF0ZT0yMzAzMDky' @@ -299,6 +205,21 @@ def service_sms_sender(request): } +@pytest.fixture +def sample_twilio_delivery_status(): + """Take a TwilioStatus mapping and generate a body and signature to match.""" + + def _wrapper(twilio_status: TwilioStatus): + return generate_twilio_signature_and_body( + token=FAKE_DELIVERY_STATUS_TOKEN, + uri=FAKE_DELIVERY_STATUS_URI, + error_code=str(twilio_status.code), + message_status='failed', + ) + + yield _wrapper + + @pytest.fixture def twilio_sms_client_mock(mocker): client = TwilioSMSClient('CREDS', 'CREDS') @@ -376,26 +297,20 @@ def test_notification_mapping(event, twilio_sms_client_mock): @pytest.mark.parametrize( - 'event', + 'twilio_status', [ - MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30001, - MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30002, - MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30003, - MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30004, - MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30005, - MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30006, - MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30007, - MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30008, - MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30009, - MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30010, - MESSAAGE_BODY_WITH_FAILED_STATUS_AND_ERROR_CODE_30034, - MESSAAGE_BODY_WITH_FAILED_STATUS_AND_INVALID_ERROR_CODE, + *TwilioSMSClient.twilio_error_code_map.values(), + TwilioStatus(-1, NOTIFICATION_TECHNICAL_FAILURE, 'Technical error'), ], + ids=[*TwilioSMSClient.twilio_error_code_map.keys(), 'invalid-error-code'], ) -def test_error_code_mapping(event, twilio_sms_client_mock): - translation: SmsStatusRecord = twilio_sms_client_mock.translate_delivery_status(event['message']) +def test_delivery_status_error_code_mapping( + twilio_status: TwilioStatus, twilio_sms_client_mock, sample_twilio_delivery_status +): + _, msg = sample_twilio_delivery_status(twilio_status) + translation: SmsStatusRecord = twilio_sms_client_mock.translate_delivery_status(msg) - assert translation.status == event['twilio_status'] + assert translation.status == twilio_status.status assert translation.status_reason is not None