diff --git a/app/forms/idv/inherited_proofing/base_form.rb b/app/forms/idv/inherited_proofing/base_form.rb index 63559f6575a..16e144690ef 100644 --- a/app/forms/idv/inherited_proofing/base_form.rb +++ b/app/forms/idv/inherited_proofing/base_form.rb @@ -11,23 +11,9 @@ def model_name def namespaced_model_name self.to_s.gsub('::', '') end - - def fields - @fields ||= required_fields + optional_fields - end - - def required_fields - raise NotImplementedError, - 'Override this method and return an Array of required field names as Symbols' - end - - def optional_fields - raise NotImplementedError, - 'Override this method and return an Array of optional field names as Symbols' - end end - private_class_method :namespaced_model_name, :required_fields, :optional_fields + private_class_method :namespaced_model_name attr_reader :payload_hash @@ -35,16 +21,12 @@ def initialize(payload_hash:) raise ArgumentError, 'payload_hash is blank?' if payload_hash.blank? raise ArgumentError, 'payload_hash is not a Hash' unless payload_hash.is_a? Hash - self.class.attr_accessor(*self.class.fields) - @payload_hash = payload_hash.dup populate_field_data end def submit - validate - FormResponse.new( success: valid?, errors: errors, diff --git a/app/forms/idv/inherited_proofing/va/form.rb b/app/forms/idv/inherited_proofing/va/form.rb index 5f8a0874c47..97878154d4a 100644 --- a/app/forms/idv/inherited_proofing/va/form.rb +++ b/app/forms/idv/inherited_proofing/va/form.rb @@ -2,18 +2,34 @@ module Idv module InheritedProofing module Va class Form < Idv::InheritedProofing::BaseForm - class << self - def required_fields - @required_fields ||= %i[first_name last_name birth_date ssn address_street address_zip] - end - - def optional_fields - @optional_fields ||= %i[phone address_street2 address_city address_state - address_country] - end - end + REQUIRED_FIELDS = %i[first_name + last_name + birth_date + ssn + address_street + address_zip].freeze + OPTIONAL_FIELDS = %i[phone + address_street2 + address_city + address_state + address_country + service_error].freeze + FIELDS = (REQUIRED_FIELDS + OPTIONAL_FIELDS).freeze + + attr_accessor(*FIELDS) + validate :add_service_error, if: :service_error? + validates(*REQUIRED_FIELDS, presence: true, unless: :service_error?) - validates(*required_fields, presence: true) + def submit + extra = {} + extra = { service_error: service_error } if service_error? + + FormResponse.new( + success: validate, + errors: errors, + extra: extra, + ) + end def user_pii raise 'User PII is invalid' unless valid? @@ -30,6 +46,22 @@ def user_pii user_pii[:zipcode] = address_zip user_pii end + + def service_error? + service_error.present? + end + + private + + def add_service_error + errors.add( + :service_provider, + # Use a "safe" error message for the model in case it's displayed + # to the user at any point. + I18n.t('inherited_proofing.errors.service_provider.communication'), + type: :service_error, + ) + end end end end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index b603263bd25..21d647edbfa 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -621,6 +621,12 @@ def idv_inherited_proofing_get_started_visited(flow_path:, step:, **extra) ) end + # Retry retrieving the user PII in the case where the first attempt fails + # in the agreement step, and the user initiates a "retry". + def idv_inherited_proofing_redo_retrieve_user_info_submitted(**extra) + track_event('IdV: inherited proofing retry retrieve user information submitted', **extra) + end + # @param [String] flow_path Document capture path ("hybrid" or "standard") # The user visited the in person proofing location step def idv_in_person_location_visited(flow_path:, **extra) diff --git a/app/services/flow/base_flow.rb b/app/services/flow/base_flow.rb index 8bcf66618be..6f5779794e7 100644 --- a/app/services/flow/base_flow.rb +++ b/app/services/flow/base_flow.rb @@ -1,5 +1,7 @@ module Flow class BaseFlow + include Failure + attr_accessor :flow_session attr_reader :steps, :actions, :current_user, :current_sp, :params, :request, :json, :http_status, :controller diff --git a/app/services/flow/base_step.rb b/app/services/flow/base_step.rb index f138d0d15df..c6f55650d25 100644 --- a/app/services/flow/base_step.rb +++ b/app/services/flow/base_step.rb @@ -1,6 +1,7 @@ module Flow class BaseStep include Rails.application.routes.url_helpers + include Failure def initialize(flow, name) @flow = flow @@ -51,13 +52,6 @@ def form_submit FormResponse.new(success: true) end - def failure(message, extra = nil) - flow_session[:error_message] = message - form_response_params = { success: false, errors: { message: message } } - form_response_params[:extra] = extra unless extra.nil? - FormResponse.new(**form_response_params) - end - def flow_params params[@name] end diff --git a/app/services/flow/failure.rb b/app/services/flow/failure.rb new file mode 100644 index 00000000000..c84beedae71 --- /dev/null +++ b/app/services/flow/failure.rb @@ -0,0 +1,12 @@ +module Flow + module Failure + private + + def failure(message, extra = nil) + flow_session[:error_message] = message + form_response_params = { success: false, errors: { message: message } } + form_response_params[:extra] = extra unless extra.nil? + FormResponse.new(**form_response_params) + end + end +end diff --git a/app/services/idv/actions/inherited_proofing/redo_retrieve_user_info_action.rb b/app/services/idv/actions/inherited_proofing/redo_retrieve_user_info_action.rb new file mode 100644 index 00000000000..a9fd7ed2c2e --- /dev/null +++ b/app/services/idv/actions/inherited_proofing/redo_retrieve_user_info_action.rb @@ -0,0 +1,19 @@ +module Idv + module Actions + module InheritedProofing + class RedoRetrieveUserInfoAction < Idv::Steps::InheritedProofing::VerifyWaitStepShow + class << self + def analytics_submitted_event + :idv_inherited_proofing_redo_retrieve_user_info_submitted + end + end + + def call + enqueue_job unless api_call_already_in_progress? + + super + end + end + end + end +end diff --git a/app/services/idv/flows/inherited_proofing_flow.rb b/app/services/idv/flows/inherited_proofing_flow.rb index 94a559c537c..2fc56d71103 100644 --- a/app/services/idv/flows/inherited_proofing_flow.rb +++ b/app/services/idv/flows/inherited_proofing_flow.rb @@ -18,7 +18,9 @@ class InheritedProofingFlow < Flow::BaseFlow { name: :secure_account }, ].freeze - ACTIONS = {}.freeze + ACTIONS = { + redo_retrieve_user_info: Idv::Actions::InheritedProofing::RedoRetrieveUserInfoAction, + }.freeze attr_reader :idv_session diff --git a/app/services/idv/inherited_proofing/va/mocks/service.rb b/app/services/idv/inherited_proofing/va/mocks/service.rb index fc1ed38269f..e1150a9528d 100644 --- a/app/services/idv/inherited_proofing/va/mocks/service.rb +++ b/app/services/idv/inherited_proofing/va/mocks/service.rb @@ -36,7 +36,7 @@ class Service }.freeze ERROR_HASH = { - errors: 'InheritedProofing::Errors::MHVIdentityDataNotFoundError', + service_error: 'the server responded with status 401', }.freeze def initialize(service_provider_data) diff --git a/app/services/idv/inherited_proofing/va/service.rb b/app/services/idv/inherited_proofing/va/service.rb index fc7e70cd91e..7e7c76a7e28 100644 --- a/app/services/idv/inherited_proofing/va/service.rb +++ b/app/services/idv/inherited_proofing/va/service.rb @@ -17,12 +17,35 @@ def initialize(service_provider_data) def execute raise 'The provided auth_code is blank?' if auth_code.blank? - response = request - payload_to_hash decrypt_payload(response) + begin + response = request + return payload_to_hash decrypt_payload(response) if response.status == 200 + + service_error(not_200_service_error(response.status)) + rescue => error + service_error(error.message) + end end private + def service_error(message) + { service_error: message } + end + + def not_200_service_error(http_status) + # Under certain circumstances, Faraday may return a nil http status. + # https://lostisland.github.io/faraday/middleware/raise-error + if http_status.blank? + http_status = 'unavailable' + http_status_description = 'unavailable' + else + http_status_description = Rack::Utils::HTTP_STATUS_CODES[http_status] + end + "The service provider API returned an http status other than 200: " \ + "#{http_status} (#{http_status_description})" + end + def request connection.get(request_uri) { |req| req.headers = request_headers } end diff --git a/app/services/idv/steps/inherited_proofing/agreement_step.rb b/app/services/idv/steps/inherited_proofing/agreement_step.rb index c3b125b24c0..b49e3d7ec7d 100644 --- a/app/services/idv/steps/inherited_proofing/agreement_step.rb +++ b/app/services/idv/steps/inherited_proofing/agreement_step.rb @@ -2,6 +2,8 @@ module Idv module Steps module InheritedProofing class AgreementStep < VerifyBaseStep + include UserPiiJobInitiator + delegate :controller, :idv_session, to: :@flow STEP_INDICATOR_STEP = :getting_started @@ -24,28 +26,6 @@ def form_submit def consent_form_params params.require(:inherited_proofing).permit(:ial2_consent_given) end - - def enqueue_job - return if api_call_already_in_progress? - - doc_capture_session = create_document_capture_session( - inherited_proofing_verify_step_document_capture_session_uuid_key, - ) - - doc_capture_session.create_proofing_session - - InheritedProofingJob.perform_later( - controller.inherited_proofing_service_provider, - controller.inherited_proofing_service_provider_data, - doc_capture_session.uuid, - ) - end - - def api_call_already_in_progress? - DocumentCaptureSession.find_by( - uuid: flow_session['inherited_proofing_verify_step_document_capture_session_uuid'], - )&.in_progress? - end end end end diff --git a/app/services/idv/steps/inherited_proofing/user_pii_job_initiator.rb b/app/services/idv/steps/inherited_proofing/user_pii_job_initiator.rb new file mode 100644 index 00000000000..3d39f3b0599 --- /dev/null +++ b/app/services/idv/steps/inherited_proofing/user_pii_job_initiator.rb @@ -0,0 +1,35 @@ +module Idv + module Steps + module InheritedProofing + module UserPiiJobInitiator + private + + def enqueue_job + return if api_call_already_in_progress? + + create_document_capture_session( + inherited_proofing_verify_step_document_capture_session_uuid_key, + ).tap do |doc_capture_session| + doc_capture_session.create_proofing_session + + InheritedProofingJob.perform_later( + controller.inherited_proofing_service_provider, + controller.inherited_proofing_service_provider_data, + doc_capture_session.uuid, + ) + end + end + + def api_call_already_in_progress? + DocumentCaptureSession.find_by( + uuid: flow_session[inherited_proofing_verify_step_document_capture_session_uuid_key], + ).present? + end + + def delete_async + flow_session.delete(inherited_proofing_verify_step_document_capture_session_uuid_key) + end + end + end + end +end diff --git a/app/services/idv/steps/inherited_proofing/verify_wait_step_show.rb b/app/services/idv/steps/inherited_proofing/verify_wait_step_show.rb index 45df7920b8c..611c8d8731d 100644 --- a/app/services/idv/steps/inherited_proofing/verify_wait_step_show.rb +++ b/app/services/idv/steps/inherited_proofing/verify_wait_step_show.rb @@ -2,6 +2,7 @@ module Idv module Steps module InheritedProofing class VerifyWaitStepShow < VerifyBaseStep + include UserPiiJobInitiator include UserPiiManagable include Idv::InheritedProofing::ServiceProviderForms delegate :controller, :idv_session, to: :@flow @@ -21,17 +22,15 @@ def call private def process_async_state(current_async_state) + return if current_async_state.in_progress? + if current_async_state.none? mark_step_incomplete(:agreement) - elsif current_async_state.in_progress? - nil elsif current_async_state.missing? flash[:error] = I18n.t('idv.failure.timeout') - # Need to add path to error pages once they exist - # LG-7257 - # This method overrides VerifyBaseStep#process_async_state: - # See the VerifyBaseStep#process_async_state "elsif current_async_state.missing?" - # logic as to what is typically needed/performed when hitting this logic path. + delete_async + mark_step_incomplete(:agreement) + @flow.analytics.idv_proofing_resolution_result_missing elsif current_async_state.done? async_state_done(current_async_state) end @@ -54,11 +53,16 @@ def async_state_done(_current_async_state) ) form_response = form.submit + delete_async + if form_response.success? inherited_proofing_save_user_pii_to_session!(form.user_pii) mark_step_complete(:verify_wait) + elsif throttle.throttled? + idv_failure(form_response) else - mark_step_incomplete(:agreement) + mark_step_complete(:agreement) + idv_failure(form_response) end form_response @@ -76,6 +80,34 @@ def document_capture_session def api_job_result document_capture_session.load_proofing_result end + + # Base class overrides + + def throttle + @throttle ||= Throttle.new( + user: current_user, + throttle_type: :inherited_proofing, + ) + end + + def idv_failure_log_throttled + @flow.analytics.throttler_rate_limit_triggered( + throttle_type: throttle.throttle_type, + step_name: self.class.name, + ) + end + + def throttled_url + idv_inherited_proofing_errors_failure_url(flow: :inherited_proofing) + end + + def exception_url + idv_inherited_proofing_errors_failure_url(flow: :inherited_proofing) + end + + def warning_url + idv_inherited_proofing_errors_no_information_url(flow: :inherited_proofing) + end end end end diff --git a/app/services/idv/steps/verify_base_step.rb b/app/services/idv/steps/verify_base_step.rb index 7a0e912f9f6..9865bac8fbb 100644 --- a/app/services/idv/steps/verify_base_step.rb +++ b/app/services/idv/steps/verify_base_step.rb @@ -78,26 +78,41 @@ def throttle def idv_failure(result) throttle.increment! if result.extra.dig(:proofing_results, :exception).blank? if throttle.throttled? - @flow.irs_attempts_api_tracker.idv_verification_rate_limited - @flow.analytics.throttler_rate_limit_triggered( - throttle_type: :idv_resolution, - step_name: self.class.name, - ) - redirect_to idv_session_errors_failure_url + idv_failure_log_throttled + redirect_to throttled_url elsif result.extra.dig(:proofing_results, :exception).present? - @flow.analytics.idv_doc_auth_exception_visited( - step_name: self.class.name, - remaining_attempts: throttle.remaining_count, - ) + idv_failure_log_error redirect_to exception_url else - @flow.analytics.idv_doc_auth_warning_visited( - step_name: self.class.name, - remaining_attempts: throttle.remaining_count, - ) + idv_failure_log_warning redirect_to warning_url end - result + end + + def idv_failure_log_throttled + @flow.irs_attempts_api_tracker.idv_verification_rate_limited + @flow.analytics.throttler_rate_limit_triggered( + throttle_type: :idv_resolution, + step_name: self.class.name, + ) + end + + def idv_failure_log_error + @flow.analytics.idv_doc_auth_exception_visited( + step_name: self.class.name, + remaining_attempts: throttle.remaining_count, + ) + end + + def idv_failure_log_warning + @flow.analytics.idv_doc_auth_warning_visited( + step_name: self.class.name, + remaining_attempts: throttle.remaining_count, + ) + end + + def throttled_url + idv_session_errors_failure_url end def exception_url diff --git a/app/services/throttle.rb b/app/services/throttle.rb index df0e89c150e..c0fc4bf7e2b 100644 --- a/app/services/throttle.rb +++ b/app/services/throttle.rb @@ -49,6 +49,10 @@ class Throttle max_attempts: IdentityConfig.store.phone_confirmation_max_attempts, attempt_window: IdentityConfig.store.phone_confirmation_max_attempt_window_in_minutes, }, + inherited_proofing: { + max_attempts: IdentityConfig.store.inherited_proofing_max_attempts, + attempt_window: IdentityConfig.store.inherited_proofing_max_attempt_window_in_minutes, + }, }.with_indifferent_access.freeze def initialize(throttle_type:, user: nil, target: nil) diff --git a/app/views/idv/inherited_proofing_errors/warning.html.erb b/app/views/idv/inherited_proofing_errors/warning.html.erb index b06fa1930bb..58044bd9f77 100644 --- a/app/views/idv/inherited_proofing_errors/warning.html.erb +++ b/app/views/idv/inherited_proofing_errors/warning.html.erb @@ -8,7 +8,7 @@ <%= t('inherited_proofing.errors.cannot_retrieve.info') %>

- <%= link_to t('inherited_proofing.buttons.try_again'), root_url, class: 'usa-button usa-button--big usa-button--wide' %> + <%= button_to t('inherited_proofing.buttons.try_again'), idv_inherited_proofing_step_path(step: :redo_retrieve_user_info), method: :put, class: 'usa-button usa-button--big usa-button--wide' %> <%= render( 'shared/troubleshooting_options', diff --git a/config/application.yml.default b/config/application.yml.default index 91df708da7d..355f85ebf3d 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -127,6 +127,8 @@ in_person_completion_survey_url: 'https://login.gov' include_slo_in_saml_metadata: false inherited_proofing_enabled: false inherited_proofing_va_base_url: 'https://staging-api.va.gov' +inherited_proofing_max_attempts: 2 +inherited_proofing_max_attempt_window_in_minutes: 1 va_inherited_proofing_mock_enabled: false irs_attempt_api_audience: 'https://irs.gov' irs_attempt_api_auth_tokens: '' diff --git a/config/locales/inherited_proofing/en.yml b/config/locales/inherited_proofing/en.yml index 6914473751b..c25dad3569c 100644 --- a/config/locales/inherited_proofing/en.yml +++ b/config/locales/inherited_proofing/en.yml @@ -27,6 +27,8 @@ en: info: We are temporarily having trouble retrieving your information. Please try again. title: Couldn’t retrieve information + service_provider: + communication: 'communication was unsuccessful' headings: lets_go: How verifying your identity works retrieval: We are retrieving your information from My HealtheVet diff --git a/config/locales/inherited_proofing/es.yml b/config/locales/inherited_proofing/es.yml index 228bc52bc0a..dc6d924f0a4 100644 --- a/config/locales/inherited_proofing/es.yml +++ b/config/locales/inherited_proofing/es.yml @@ -28,6 +28,8 @@ es: info: Estamos teniendo problemas temporalmente para obtener su información. Inténtelo de nuevo. title: to be implemented + service_provider: + communication: 'la comunicacion no tuvo exito' headings: lets_go: to be implemented retrieval: Estamos recuperando su información de My HealtheVet diff --git a/config/locales/inherited_proofing/fr.yml b/config/locales/inherited_proofing/fr.yml index 209686a40aa..0ff9ad3878e 100644 --- a/config/locales/inherited_proofing/fr.yml +++ b/config/locales/inherited_proofing/fr.yml @@ -29,6 +29,8 @@ fr: info: Nous avons temporairement des difficultés à récupérer vos informations. Veuillez réessayer. title: to be implemented + service_provider: + communication: 'la communication a échoué' headings: lets_go: to be implemented retrieval: Nous récupérons vos informations depuis My HealtheVet. diff --git a/lib/identity_config.rb b/lib/identity_config.rb index e3a734946ae..d6214abc001 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -206,6 +206,8 @@ def self.build_store(config_map) config.add(:include_slo_in_saml_metadata, type: :boolean) config.add(:inherited_proofing_enabled, type: :boolean) config.add(:inherited_proofing_va_base_url, type: :string) + config.add(:inherited_proofing_max_attempts, type: :integer) + config.add(:inherited_proofing_max_attempt_window_in_minutes, type: :integer) config.add(:va_inherited_proofing_mock_enabled, type: :boolean) config.add(:irs_attempt_api_audience) config.add(:irs_attempt_api_auth_tokens, type: :comma_separated_string_list) diff --git a/spec/features/idv/inherited_proofing/inherited_proofing_cancel_spec.rb b/spec/features/idv/inherited_proofing/inherited_proofing_cancel_spec.rb index d3be7768af1..cf594bd0c6c 100644 --- a/spec/features/idv/inherited_proofing/inherited_proofing_cancel_spec.rb +++ b/spec/features/idv/inherited_proofing/inherited_proofing_cancel_spec.rb @@ -1,66 +1,19 @@ require 'rails_helper' -# Simulates a user (in this case, a VA inherited proofing-authorized user) -# coming over to login.gov from a service provider, and hitting the -# OpenidConnect::AuthorizationController#index action. -def send_user_from_service_provider_to_login_gov_openid_connect(user) - expect(user).to_not be_nil - # NOTE: VA user. - visit_idp_from_oidc_va_with_ial2 -end - -def complete_idv_steps_up_to_inherited_proofing_get_started_step(user, expect_accessible: false) - unless current_path == idv_inherited_proofing_step_path(step: :get_started) - complete_idv_steps_before_phone_step(user) - click_link t('links.cancel') - click_button t('idv.cancel.actions.start_over') - expect(page).to have_current_path(idv_inherited_proofing_step_path(step: :get_started)) - end - expect(page).to be_axe_clean.according_to :section508, :"best-practice" if expect_accessible -end - -def complete_idv_steps_up_to_inherited_proofing_how_verifying_step(user, expect_accessible: false) - complete_idv_steps_up_to_inherited_proofing_get_started_step user, - expect_accessible: expect_accessible - unless current_path == idv_inherited_proofing_step_path(step: :agreement) - click_on t('inherited_proofing.buttons.continue') - end -end - -def complete_idv_steps_up_to_inherited_proofing_we_are_retrieving_step(user, - expect_accessible: false) - complete_idv_steps_up_to_inherited_proofing_how_verifying_step( - user, - expect_accessible: expect_accessible, - ) - unless current_path == idv_inherited_proofing_step_path(step: :verify_wait) - check t('inherited_proofing.instructions.consent', app_name: APP_NAME), allow_label_click: true - click_on t('inherited_proofing.buttons.continue') - end -end - -def complete_idv_steps_up_to_inherited_proofing_verify_your_info_step(user, - expect_accessible: false) - complete_idv_steps_up_to_inherited_proofing_we_are_retrieving_step( - user, - expect_accessible: expect_accessible, - ) -end - feature 'inherited proofing cancel process', :js do - include InheritedProofingHelper - include_context 'va_user_context' + include InheritedProofingWithServiceProviderHelper before do allow(IdentityConfig.store).to receive(:va_inherited_proofing_mock_enabled).and_return true - send_user_from_service_provider_to_login_gov_openid_connect user + send_user_from_service_provider_to_login_gov_openid_connect user, inherited_proofing_auth end let!(:user) { user_with_2fa } + let(:inherited_proofing_auth) { Idv::InheritedProofing::Va::Mocks::Service::VALID_AUTH_CODE } context 'from the "Get started verifying your identity" view, and clicking the "Cancel" link' do before do - complete_idv_steps_up_to_inherited_proofing_get_started_step user + complete_steps_up_to_inherited_proofing_get_started_step user end it 'should have current path equal to the Getting Started page' do @@ -106,7 +59,7 @@ def complete_idv_steps_up_to_inherited_proofing_verify_your_info_step(user, context 'from the "How verifying your identify works" view, and clicking the "Cancel" link' do before do - complete_idv_steps_up_to_inherited_proofing_how_verifying_step user + complete_steps_up_to_inherited_proofing_how_verifying_step user end it 'should have current path equal to the How Verifying (agreement step) page' do @@ -152,7 +105,7 @@ def complete_idv_steps_up_to_inherited_proofing_verify_your_info_step(user, context 'from the "Verify your information..." view, and clicking the "Cancel" link' do before do - complete_idv_steps_up_to_inherited_proofing_verify_your_info_step user + complete_steps_up_to_inherited_proofing_verify_your_info_step user end it 'should have current path equal to the Verify your information (verify_info step) page' do diff --git a/spec/features/idv/inherited_proofing/verify_wait_step_spec.rb b/spec/features/idv/inherited_proofing/verify_wait_step_spec.rb new file mode 100644 index 00000000000..3c17b33d889 --- /dev/null +++ b/spec/features/idv/inherited_proofing/verify_wait_step_spec.rb @@ -0,0 +1,73 @@ +require 'rails_helper' + +feature 'inherited proofing verify wait', :js do + include InheritedProofingWithServiceProviderHelper + + before do + allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) + allow(IdentityConfig.store).to receive(:va_inherited_proofing_mock_enabled).and_return(true) + send_user_from_service_provider_to_login_gov_openid_connect(user, inherited_proofing_auth) + end + + let!(:user) { user_with_2fa } + let(:inherited_proofing_auth) { Idv::InheritedProofing::Va::Mocks::Service::VALID_AUTH_CODE } + let(:fake_analytics) { FakeAnalytics.new } + + context 'when on the "How verifying your identify works" page, ' \ + 'and the user clicks the "Continue" button' do + before do + complete_steps_up_to_inherited_proofing_we_are_retrieving_step user + end + + context 'when there are no service-related errors' do + it 'displays the "Verify your information" page' do + expect(page).to have_current_path( + idv_inherited_proofing_step_path(step: :verify_info), + ) + end + end + + context 'when there are service-related errors on the first attempt' do + let(:inherited_proofing_auth) { 'invalid-auth-code' } + + it 'displays the warning page and allows retries' do + expect(page).to have_current_path( + idv_inherited_proofing_errors_no_information_path(flow: :inherited_proofing), + ) + expect(page).to have_selector(:link_or_button, t('inherited_proofing.buttons.try_again')) + end + end + + context 'when there are service-related errors on the second attempt' do + let(:inherited_proofing_auth) { 'invalid-auth-code' } + + it 'redirects to the error page, prohibits retries and logs the event' do + click_button t('inherited_proofing.buttons.try_again') + expect(page).to have_current_path( + idv_inherited_proofing_errors_failure_url(flow: :inherited_proofing), + ) + expect(fake_analytics).to have_logged_event( + 'Throttler Rate Limit Triggered', + throttle_type: :inherited_proofing, + step_name: Idv::Actions::InheritedProofing::RedoRetrieveUserInfoAction.name, + ) + end + end + end + + context 'when the async state is missing during polling' do + before do + allow_any_instance_of(ProofingSessionAsyncResult).to receive(:missing?).and_return(true) + complete_steps_up_to_inherited_proofing_we_are_retrieving_step user + end + + it 'redirects back to the agreement step and logs the event' do + expect(page).to have_current_path( + idv_inherited_proofing_step_path(step: :agreement), + ) + expect(fake_analytics).to have_logged_event( + 'Proofing Resolution Result Missing', + ) + end + end +end diff --git a/spec/forms/idv/inherited_proofing/base_form_spec.rb b/spec/forms/idv/inherited_proofing/base_form_spec.rb index de84e22d7ea..871142de3eb 100644 --- a/spec/forms/idv/inherited_proofing/base_form_spec.rb +++ b/spec/forms/idv/inherited_proofing/base_form_spec.rb @@ -40,24 +40,6 @@ def user_pii; {} end describe '#initialize' do subject { form_class } - context 'when .required_fields is not overridden' do - it 'raises an error' do - subject.singleton_class.send(:remove_method, :required_fields) - expected_error = 'Override this method and return ' \ - 'an Array of required field names as Symbols' - expect { subject.new(payload_hash: payload_hash) }.to raise_error(expected_error) - end - end - - context 'when .optional_fields is not overridden' do - it 'raises an error' do - subject.singleton_class.send(:remove_method, :optional_fields) - expected_error = 'Override this method and return ' \ - 'an Array of optional field names as Symbols' - expect { subject.new(payload_hash: payload_hash) }.to raise_error(expected_error) - end - end - context 'when .user_pii is not overridden' do subject do Class.new(Idv::InheritedProofing::BaseForm) do @@ -82,31 +64,6 @@ def optional_fields; [] end expect(described_class.model_name).to eq 'IdvInheritedProofingBaseForm' end end - - describe '.fields' do - subject do - Class.new(Idv::InheritedProofing::BaseForm) do - class << self - def required_fields; %i[required] end - - def optional_fields; %i[optional] end - end - - def user_pii; {} end - end - end - - let(:expected_field_names) do - [ - :required, - :optional, - ].sort - end - - it 'returns the right field names' do - expect(subject.fields).to match_array expected_field_names - end - end end describe '#initialize' do @@ -217,8 +174,8 @@ def user_pii; {} end subject.submit end - it 'calls #validate' do - expect(subject).to receive(:validate).once + it 'calls #valid?' do + expect(subject).to receive(:valid?).once end end end diff --git a/spec/forms/idv/inherited_proofing/va/form_spec.rb b/spec/forms/idv/inherited_proofing/va/form_spec.rb index 050b3f6ca7a..ad734ab37c1 100644 --- a/spec/forms/idv/inherited_proofing/va/form_spec.rb +++ b/spec/forms/idv/inherited_proofing/va/form_spec.rb @@ -4,7 +4,9 @@ subject(:form) { described_class.new payload_hash: payload_hash } let(:required_fields) { %i[first_name last_name birth_date ssn address_street address_zip] } - let(:optional_fields) { %i[phone address_street2 address_city address_state address_country] } + let(:optional_fields) do + %i[phone address_street2 address_city address_state address_country service_error] + end let(:payload_hash) do { @@ -24,28 +26,30 @@ } end - describe 'class methods' do - describe '.model_name' do - it 'returns the right model name' do - expect(described_class.model_name).to eq 'IdvInheritedProofingVaForm' - end - end - - describe '.fields' do + describe 'constants' do + describe 'FIELDS' do it 'returns all the fields' do - expect(described_class.fields).to match_array required_fields + optional_fields + expect(described_class::FIELDS).to match_array required_fields + optional_fields end end - describe '.required_fields' do + describe 'REQUIRED_FIELDS' do it 'returns the required fields' do - expect(described_class.required_fields).to match_array required_fields + expect(described_class::REQUIRED_FIELDS).to match_array required_fields end end - describe '.optional_fields' do + describe 'OPTIONAL_FIELDS' do it 'returns the optional fields' do - expect(described_class.optional_fields).to match_array optional_fields + expect(described_class::OPTIONAL_FIELDS).to match_array optional_fields + end + end + end + + describe 'class methods' do + describe '.model_name' do + it 'returns the right model name' do + expect(described_class.model_name).to eq 'IdvInheritedProofingVaForm' end end end @@ -56,7 +60,7 @@ let(:payload_hash) { :x } it 'raises an error' do - expect { subject }.to raise_error 'payload_hash is not a Hash' + expect { form }.to raise_error 'payload_hash is not a Hash' end end @@ -75,7 +79,7 @@ context 'when passing a valid payload hash' do it 'raises no errors' do - expect { subject }.to_not raise_error + expect { form }.to_not raise_error end end end @@ -83,7 +87,7 @@ describe '#validate' do context 'with valid payload data' do it 'returns true' do - expect(subject.validate).to eq true + expect(form.validate).to be true end end @@ -119,12 +123,12 @@ end it 'returns false' do - expect(subject.validate).to eq false + expect(form.validate).to be false end it 'adds the correct error messages for missing fields' do subject.validate - expect(subject.errors.full_messages).to match_array expected_error_messages + expect(form.errors.full_messages).to match_array expected_error_messages end end @@ -160,12 +164,12 @@ end it 'returns false' do - expect(subject.validate).to eq false + expect(form.validate).to be false end it 'adds the correct error messages for required fields that are missing data' do subject.validate - expect(subject.errors.full_messages).to match_array expected_error_messages + expect(form.errors.full_messages).to match_array expected_error_messages end end @@ -189,7 +193,24 @@ end it 'returns true' do - expect(subject.validate).to eq true + expect(form.validate).to be true + end + end + + context 'when there is a service-related error' do + before do + subject.validate + end + + let(:payload_hash) { { service_error: 'service error' } } + + it 'returns false' do + expect(form.valid?).to be false + end + + it 'adds a user-friendly model error' do + expect(form.errors.full_messages).to \ + match_array ['Service provider communication was unsuccessful'] end end end @@ -230,11 +251,33 @@ it 'returns a FormResponse indicating the correct errors and status' do form_response = subject.submit - expect(form_response.success?).to eq false + expect(form_response.success?).to be false expect(form_response.errors).to match_array expected_errors + expect(form_response.extra).to eq({}) end end end + + context 'with a valid payload' do + it 'returns a FormResponse indicating the no errors and successful status' do + form_response = subject.submit + expect(form_response.success?).to be true + expect(form_response.errors).to eq({}) + expect(form_response.extra).to eq({}) + end + end + + context 'when there is a service-related error' do + let(:payload_hash) { { service_error: 'service error' } } + + it 'adds the unfiltered error to the FormResponse :extra Hash' do + form_response = subject.submit + expect(form_response.success?).to be false + expect(form_response.errors).to \ + eq({ service_provider: ['communication was unsuccessful'] }) + expect(form_response.extra).to eq({ service_error: 'service error' }) + end + end end describe '#user_pii' do @@ -252,7 +295,23 @@ } end it 'returns the correct user pii' do - expect(subject.user_pii).to eq expected_user_pii + expect(form.user_pii).to eq expected_user_pii + end + end + + describe '#service_error?' do + context 'when there is a service-related error' do + let(:payload_hash) { { service_error: 'service error' } } + + it 'returns true' do + expect(form.service_error?).to be true + end + end + + context 'when there is not a service-related error' do + it 'returns false' do + expect(form.service_error?).to be false + end end end end diff --git a/spec/services/idv/inherited_proofing/va/service_spec.rb b/spec/services/idv/inherited_proofing/va/service_spec.rb index 81695a06e1e..7a5dece157b 100644 --- a/spec/services/idv/inherited_proofing/va/service_spec.rb +++ b/spec/services/idv/inherited_proofing/va/service_spec.rb @@ -62,5 +62,63 @@ it_behaves_like 'an invalid auth code error is raised' end end + + context 'when a request error is raised' do + before do + allow(service).to receive(:request).and_raise('Boom!') + end + + it 'rescues and returns the error' do + expect(service.execute).to eq({ service_error: 'Boom!' }) + end + end + + context 'when a decryption error is raised' do + it 'rescues and returns the error' do + freeze_time do + stub_request(:get, request_uri). + with(headers: request_headers). + to_return(status: 200, body: 'xyz', headers: {}) + + expect(service.execute[:service_error]).to match(/unexpected token at 'xyz'/) + end + end + end + + context 'when a non-200 error is raised' do + it 'rescues and returns the error' do + freeze_time do + stub_request(:get, request_uri). + with(headers: request_headers). + to_return(status: 302, body: encrypted_user_attributes, headers: {}) + + expect(service.execute.to_s).to \ + match(/The service provider API returned an http status other than 200/) + end + end + + context 'when http status is unavailable (nil)' do + before do + allow_any_instance_of(Faraday::Response).to receive(:status).and_return(nil) + end + + let(:expected_error) do + { + service_error: 'The service provider API returned an http status other than 200: ' \ + 'unavailable (unavailable)', + } + end + + it 'rescues and returns the error' do + freeze_time do + stub_request(:get, request_uri). + with(headers: request_headers). + to_return(status: nil, body: encrypted_user_attributes, headers: {}) + + expect(service.execute).to eq expected_error + end + end + end + end end end diff --git a/spec/support/features/idv_helper.rb b/spec/support/features/idv_helper.rb index c244ac17b6c..80572201f57 100644 --- a/spec/support/features/idv_helper.rb +++ b/spec/support/features/idv_helper.rb @@ -124,7 +124,8 @@ def visit_idp_from_oidc_va_with_ial2( client_id: sp_oidc_issuer, state: SecureRandom.hex, nonce: SecureRandom.hex, - verified_within: nil + verified_within: nil, + inherited_proofing_auth: Idv::InheritedProofing::Va::Mocks::Service::VALID_AUTH_CODE ) @state = state @client_id = sp_oidc_issuer @@ -139,7 +140,7 @@ def visit_idp_from_oidc_va_with_ial2( prompt: 'select_account', nonce: nonce, verified_within: verified_within, - inherited_proofing_auth: Idv::InheritedProofing::Va::Mocks::Service::VALID_AUTH_CODE, + inherited_proofing_auth: inherited_proofing_auth, ) end diff --git a/spec/support/features/inherited_proofing_with_service_provider_helper.rb b/spec/support/features/inherited_proofing_with_service_provider_helper.rb new file mode 100644 index 00000000000..f899b293d2c --- /dev/null +++ b/spec/support/features/inherited_proofing_with_service_provider_helper.rb @@ -0,0 +1,55 @@ +require_relative 'idv_step_helper' +require_relative 'doc_auth_helper' + +module InheritedProofingWithServiceProviderHelper + include IdvStepHelper + include DocAuthHelper + + # Simulates a user (in this case, a VA inherited proofing-authorized user) + # coming over to login.gov from a service provider, and hitting the + # OpenidConnect::AuthorizationController#index action. + def send_user_from_service_provider_to_login_gov_openid_connect(user, inherited_proofing_auth) + expect(user).to_not be_nil + # NOTE: VA user. + visit_idp_from_oidc_va_with_ial2 inherited_proofing_auth: inherited_proofing_auth + end + + def complete_steps_up_to_inherited_proofing_get_started_step(user, expect_accessible: false) + unless current_path == idv_inherited_proofing_step_path(step: :get_started) + complete_idv_steps_before_phone_step(user) + click_link t('links.cancel') + click_button t('idv.cancel.actions.start_over') + expect(page).to have_current_path(idv_inherited_proofing_step_path(step: :get_started)) + end + expect(page).to be_axe_clean.according_to :section508, :"best-practice" if expect_accessible + end + + def complete_steps_up_to_inherited_proofing_how_verifying_step(user, expect_accessible: false) + complete_steps_up_to_inherited_proofing_get_started_step user, + expect_accessible: expect_accessible + unless current_path == idv_inherited_proofing_step_path(step: :agreement) + click_on t('inherited_proofing.buttons.continue') + end + end + + def complete_steps_up_to_inherited_proofing_we_are_retrieving_step(user, + expect_accessible: false) + complete_steps_up_to_inherited_proofing_how_verifying_step( + user, + expect_accessible: expect_accessible, + ) + unless current_path == idv_inherited_proofing_step_path(step: :verify_wait) + check t('inherited_proofing.instructions.consent', app_name: APP_NAME), + allow_label_click: true + click_on t('inherited_proofing.buttons.continue') + end + end + + def complete_steps_up_to_inherited_proofing_verify_your_info_step(user, + expect_accessible: false) + complete_steps_up_to_inherited_proofing_we_are_retrieving_step( + user, + expect_accessible: expect_accessible, + ) + end +end