diff --git a/app/errors/twilio_errors.rb b/app/errors/twilio_errors.rb index e2f3d37f763..731552437ef 100644 --- a/app/errors/twilio_errors.rb +++ b/app/errors/twilio_errors.rb @@ -4,6 +4,7 @@ module TwilioErrors 21_211 => I18n.t('errors.messages.invalid_phone_number'), 21_215 => I18n.t('errors.messages.invalid_calling_area'), 21_614 => I18n.t('errors.messages.invalid_sms_number'), + 4_815_162_342 => I18n.t('errors.messages.twilio_timeout'), }.freeze VERIFY_ERRORS = { diff --git a/app/services/twilio_service.rb b/app/services/twilio_service.rb index fb866c21b61..1e72e04d662 100644 --- a/app/services/twilio_service.rb +++ b/app/services/twilio_service.rb @@ -7,6 +7,7 @@ class Utils end def initialize + @http_client = Twilio::HTTP::Client.new(timeout: Figaro.env.twilio_timeout.to_i) @client = if FeatureManagement.telephony_disabled? NullTwilioClient.new else @@ -41,13 +42,10 @@ def from_number private - attr_reader :client + attr_reader :client, :http_client def twilio_client - telephony_service.new( - TWILIO_SID, - TWILIO_AUTH_TOKEN - ) + telephony_service.new(TWILIO_SID, TWILIO_AUTH_TOKEN, nil, nil, @http_client) end def random_phone_number @@ -59,6 +57,8 @@ def sanitize_errors rescue Twilio::REST::RestError => error sanitize_phone_number(error.message) raise + rescue Faraday::TimeoutError + raise Twilio::REST::RestError.new('timeout', TwilioTimeoutResponse.new) end DIGITS_TO_PRESERVE = 5 @@ -72,5 +72,15 @@ def sanitize_phone_number(str) end end end + + class TwilioTimeoutResponse + def status_code + 4_815_162_342 + end + + def body + {} + end + end end end diff --git a/config/application.yml.example b/config/application.yml.example index 77a0f3c89d9..cdfa2f6fb81 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -67,6 +67,7 @@ use_dashboard_service_providers: 'false' dashboard_url: 'https://dashboard.demo.login.gov' valid_authn_contexts: '["http://idmanagement.gov/ns/assurance/loa/1", "http://idmanagement.gov/ns/assurance/loa/3"]' +twilio_timeout: '5' usps_upload_sftp_timeout: '5' development: diff --git a/config/initializers/figaro.rb b/config/initializers/figaro.rb index 268bd73444f..06de0362e43 100644 --- a/config/initializers/figaro.rb +++ b/config/initializers/figaro.rb @@ -49,6 +49,7 @@ 'twilio_auth_token', 'twilio_record_voice', 'twilio_messaging_service_sid', + 'twilio_timeout', 'use_kms', 'valid_authn_contexts' ) diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml index 423bacf7616..45509a05bc3 100644 --- a/config/locales/errors/en.yml +++ b/config/locales/errors/en.yml @@ -44,6 +44,7 @@ en: phone_unsupported: Sorry, we are unable to send SMS at this time. Please try the phone call option below, or use your personal key. twilio_inbound_sms_invalid: The inbound Twilio SMS message failed validation. + twilio_timeout: The server took too long to respond. Please try again. unauthorized_authn_context: Unauthorized authentication context unauthorized_nameid_format: Unauthorized nameID format unauthorized_service_provider: Unauthorized Service Provider diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml index ec1c8027781..62205656ae9 100644 --- a/config/locales/errors/es.yml +++ b/config/locales/errors/es.yml @@ -40,6 +40,7 @@ es: phone_unsupported: Lo sentimos, no podemos enviar SMS en este momento. Pruebe la opción de llamada telefónica a continuación o use su clave personal. twilio_inbound_sms_invalid: El mensaje de Twilio SMS de entrada falló la validación. + twilio_timeout: NOT TRANSLATED YET unauthorized_authn_context: Contexto de autenticación no autorizado unauthorized_nameid_format: NOT TRANSLATED YET unauthorized_service_provider: Proveedor de servicio no autorizado diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml index c082243df38..bab66df7b11 100644 --- a/config/locales/errors/fr.yml +++ b/config/locales/errors/fr.yml @@ -42,6 +42,7 @@ fr: le moment. S'il vous plaît essayez l'option d'appel téléphonique ci-dessous, ou utilisez votre clé personnelle. twilio_inbound_sms_invalid: Le message SMS Twilio entrant a échoué à la validation. + twilio_timeout: NOT TRANSLATED YET unauthorized_authn_context: Contexte d'authentification non autorisé unauthorized_nameid_format: NOT TRANSLATED YET unauthorized_service_provider: Fournisseur de service non autorisé diff --git a/spec/services/twilio_service_spec.rb b/spec/services/twilio_service_spec.rb index 36a577875c0..d8cc7409821 100644 --- a/spec/services/twilio_service_spec.rb +++ b/spec/services/twilio_service_spec.rb @@ -40,10 +40,15 @@ TwilioService::Utils.telephony_service = Twilio::REST::Client end - it 'uses a real Twilio client' do + it 'uses a real Twilio client with timeout' do + allow(Figaro.env).to receive(:twilio_timeout).and_return('1') client = instance_double(Twilio::REST::Client) + twilio_http_client = instance_double(Twilio::HTTP::Client) + expect(Twilio::HTTP::Client).to receive(:new).with(timeout: 1).and_return(twilio_http_client) expect(Twilio::REST::Client). - to receive(:new).with(/sid(1|2)/, /token(1|2)/).and_return(client) + to receive(:new). + with(/sid(1|2)/, /token(1|2)/, nil, nil, twilio_http_client). + and_return(client) http_client = Struct.new(:adapter) expect(client).to receive(:http_client).and_return(http_client) expect(http_client).to receive(:adapter=).with(:typhoeus) @@ -95,6 +100,21 @@ expect { service.place_call(to: '+123456789012', url: 'https://twimlet.com') }. to raise_error(Twilio::REST::RestError, sanitized_message) end + + it 'rescues timeout errors and raises a custom Twilio error' do + TwilioService::Utils.telephony_service = FakeVoiceCall + error_code = 4_815_162_342 + status_code = 4_815_162_342 + + message = "[HTTP #{status_code}] #{error_code} : timeout\n\n" + service = TwilioService::Utils.new + + expect(service.send(:client).calls).to receive(:create). + and_raise(Faraday::TimeoutError) + + expect { service.place_call(to: '+123456789012', url: 'https://twimlet.com') }. + to raise_error(Twilio::REST::RestError, message) + end end describe '#send_sms' do @@ -137,5 +157,20 @@ expect { service.send_sms(to: '+1 (888) 555-5555', body: 'test') }. to raise_error(Twilio::REST::RestError, sanitized_message) end + + it 'rescues timeout errors and raises a custom Twilio error' do + TwilioService::Utils.telephony_service = FakeSms + error_code = 4_815_162_342 + status_code = 4_815_162_342 + + message = "[HTTP #{status_code}] #{error_code} : timeout\n\n" + service = TwilioService::Utils.new + + expect(service.send(:client).messages).to receive(:create). + and_raise(Faraday::TimeoutError) + + expect { service.send_sms(to: '+123456789012', body: 'test') }. + to raise_error(Twilio::REST::RestError, message) + end end end diff --git a/spec/support/fake_sms.rb b/spec/support/fake_sms.rb index 1e4e3c62862..f884cf2e64d 100644 --- a/spec/support/fake_sms.rb +++ b/spec/support/fake_sms.rb @@ -5,7 +5,7 @@ class FakeSms cattr_accessor :messages self.messages = [] - def initialize(_account_sid, _auth_token); end + def initialize(_username, _password, _account_sid, _region, _http_client); end def messages self diff --git a/spec/support/fake_voice_call.rb b/spec/support/fake_voice_call.rb index 4a5512a3e0b..f5afeb89093 100644 --- a/spec/support/fake_voice_call.rb +++ b/spec/support/fake_voice_call.rb @@ -4,7 +4,7 @@ class FakeVoiceCall cattr_accessor :calls self.calls = [] - def initialize(_account_sid, _auth_token); end + def initialize(_username, _password, _account_sid, _region, _http_client); end def calls self