diff --git a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb index a3045d5e85f..096537afe41 100644 --- a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb @@ -36,7 +36,6 @@ def show :docvTransactionToken, ) document_capture_session.save - # useful for analytics @msg = document_response[:msg] @reference_id = document_response[:referenceId] diff --git a/app/services/doc_auth/socure/request.rb b/app/services/doc_auth/socure/request.rb index 64fa97efd05..cb7644c14a3 100644 --- a/app/services/doc_auth/socure/request.rb +++ b/app/services/doc_auth/socure/request.rb @@ -6,9 +6,11 @@ class Request def fetch # return DocAuth::Response with DocAuth::Error if workflow is invalid http_response = send_http_request - return handle_invalid_response(http_response) unless http_response.success? - - handle_http_response(http_response) + if http_response&.success? && http_response.body.present? + handle_http_response(http_response) + else + handle_invalid_response(http_response) + end rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError => e handle_connection_error(exception: e) end @@ -33,16 +35,27 @@ def handle_http_response(_response) end def handle_invalid_response(http_response) - begin - if http_response.body.present? - warn(http_response.body) - JSON.parse(http_response.body) - else - {} - end + message = [ + self.class.name, + 'Unexpected HTTP response', + http_response&.status, + ].join(' ') + exception = DocAuth::RequestError.new(message, http_response&.status) + + response_body = begin + http_response&.body.present? ? JSON.parse(http_response.body) : {} rescue JSON::JSONError {} end + handle_connection_error( + exception: exception, + status: response_body.dig('status'), + status_message: response_body.dig('msg'), + ) + end + + def handle_connection_error(exception:, status: nil, status_message: nil) + raise NotImplementedError end def send_http_get_request diff --git a/app/services/doc_auth/socure/requests/document_request.rb b/app/services/doc_auth/socure/requests/document_request.rb index 796b6cb22b2..9062ac1b9be 100644 --- a/app/services/doc_auth/socure/requests/document_request.rb +++ b/app/services/doc_auth/socure/requests/document_request.rb @@ -47,6 +47,20 @@ def handle_http_response(http_response) JSON.parse(http_response.body, symbolize_names: true) end + def handle_connection_error(exception:, status: nil, status_message: nil) + NewRelic::Agent.notice_error(exception) + { + success: false, + errors: { network: true }, + exception: exception, + extra: { + vendor: 'Socure', + vendor_status: status, + vendor_status_message: status_message, + }.compact, + } + end + def method :post end diff --git a/app/services/doc_auth/socure/requests/docv_result_request.rb b/app/services/doc_auth/socure/requests/docv_result_request.rb index 6d53cc39d7a..7f4bc26d116 100644 --- a/app/services/doc_auth/socure/requests/docv_result_request.rb +++ b/app/services/doc_auth/socure/requests/docv_result_request.rb @@ -27,6 +27,20 @@ def handle_http_response(http_response) ) end + def handle_connection_error(exception:, status: nil, status_message: nil) + NewRelic::Agent.notice_error(exception) + DocAuth::Response.new( + success: false, + errors: { network: true }, + exception: exception, + extra: { + vendor: 'Socure', + vendor_status: status, + vendor_status_message: status_message, + }.compact, + ) + end + def document_capture_session @document_capture_session ||= DocumentCaptureSession.find_by!(uuid: document_capture_session_uuid) diff --git a/app/services/doc_auth/socure/responses/docv_result_response.rb b/app/services/doc_auth/socure/responses/docv_result_response.rb index 23b17f88e9a..aaf4413da27 100644 --- a/app/services/doc_auth/socure/responses/docv_result_response.rb +++ b/app/services/doc_auth/socure/responses/docv_result_response.rb @@ -32,7 +32,7 @@ class DocvResultResponse < DocAuth::Response expiration_date: %w[documentVerification documentData expirationDate], }.freeze - def initialize(http_response: nil, biometric_comparison_required: false) + def initialize(http_response:, biometric_comparison_required: false) @http_response = http_response @biometric_comparison_required = biometric_comparison_required @pii_from_doc = read_pii @@ -110,17 +110,23 @@ def get_data(path) end def parsed_response_body - @parsed_response_body ||= JSON.parse(http_response.body).with_indifferent_access + @parsed_response_body ||= begin + http_response&.body.present? ? JSON.parse( + http_response.body, + ).with_indifferent_access : {} + rescue JSON::JSONError + {} + end end def state_id_type type = get_data(DATA_PATHS[:id_type]) - type.gsub(/\W/, '').underscore + type&.gsub(/\W/, '')&.underscore end def parse_date(date_string) Date.parse(date_string) - rescue ArgumentError + rescue ArgumentError, TypeError message = { event: 'Failure to parse Socure ID+ date', }.to_json diff --git a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb index 8cee30758b4..8842563e102 100644 --- a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb @@ -4,7 +4,7 @@ include FlowPolicyHelper let(:idv_vendor) { Idp::Constants::Vendors::SOCURE } - let(:fake_socure_endpoint) { 'https://fake-socure.com' } + let(:fake_socure_endpoint) { 'https://fake-socure.test' } let(:user) { create(:user) } let(:stored_result) { nil } let(:socure_enabled) { true } @@ -82,6 +82,7 @@ before do allow(I18n).to receive(:locale).and_return(expected_language) allow(request_class).to receive(:new).and_call_original + allow(request_class).to receive(:handle_connection_error).and_call_original get(:show) end @@ -175,6 +176,74 @@ expect(response).to be_not_found end end + + context 'when socure error encountered' do + let(:fake_socure_endpoint) { 'https://fake-socure.test/' } + let(:failed_response_body) do + { 'status' => 'Error', + 'referenceId' => '1cff6d33-1cc0-4205-b740-c9a9e6b8bd66', + 'data' => {}, + 'msg' => 'No active account is associated with this request' } + end + let(:response_body_401) do + { + status: 'Error', + referenceId: '7ff0cdc5-395e-45d1-8467-0ff1b41c11dc', + msg: 'string', + } + end + let(:no_doc_found_response_body) do + { + referenceId: '0dc21b0d-04df-4dd5-8533-ec9ecdafe0f4', + msg: { + status: 400, + msg: 'No Documents found', + }, + } + end + before do + allow(IdentityConfig.store).to receive(:socure_document_request_endpoint). + and_return(fake_socure_endpoint) + end + it 'connection timeout still responds to user' do + stub_request(:post, fake_socure_endpoint).to_raise(Faraday::ConnectionFailed) + get(:show) + expect(response).to be_ok + end + + it 'socure error response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 401, + body: JSON.generate(failed_response_body), + ) + get(:show) + expect(response).to be_ok + end + it 'socure nil response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 500, + body: nil, + ) + get(:show) + expect(response).to be_ok + end + it 'socure nil response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 401, + body: JSON.generate(response_body_401), + ) + get(:show) + expect(response).to be_ok + end + it 'socure nil response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 401, + body: JSON.generate(no_doc_found_response_body), + ) + get(:show) + expect(response).to be_ok + end + end end describe '#update' do diff --git a/spec/controllers/idv/socure/document_capture_controller_spec.rb b/spec/controllers/idv/socure/document_capture_controller_spec.rb index 863f553eb3e..0207691f9e8 100644 --- a/spec/controllers/idv/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/socure/document_capture_controller_spec.rb @@ -4,7 +4,7 @@ include FlowPolicyHelper let(:idv_vendor) { Idp::Constants::Vendors::SOCURE } - let(:fake_socure_endpoint) { 'https://fake-socure.com' } + let(:fake_socure_endpoint) { 'https://fake-socure.test' } let(:user) { create(:user) } let(:stored_result) { nil } let(:socure_enabled) { true } @@ -175,6 +175,74 @@ expect(response).to be_not_found end end + + context 'when socure error encountered' do + let(:fake_socure_endpoint) { 'https://fake-socure.test/' } + let(:failed_response_body) do + { 'status' => 'Error', + 'referenceId' => '1cff6d33-1cc0-4205-b740-c9a9e6b8bd66', + 'data' => {}, + 'msg' => 'No active account is associated with this request' } + end + let(:response_body_401) do + { + status: 'Error', + referenceId: '7ff0cdc5-395e-45d1-8467-0ff1b41c11dc', + msg: 'string', + } + end + let(:no_doc_found_response_body) do + { + referenceId: '0dc21b0d-04df-4dd5-8533-ec9ecdafe0f4', + msg: { + status: 400, + msg: 'No Documents found', + }, + } + end + before do + allow(IdentityConfig.store).to receive(:socure_document_request_endpoint). + and_return(fake_socure_endpoint) + end + it 'connection timeout still responds to user' do + stub_request(:post, fake_socure_endpoint).to_raise(Faraday::ConnectionFailed) + get(:show) + expect(response).to be_ok + end + + it 'socure error response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 401, + body: JSON.generate(failed_response_body), + ) + get(:show) + expect(response).to be_ok + end + it 'socure nil response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 500, + body: nil, + ) + get(:show) + expect(response).to be_ok + end + it 'socure nil response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 401, + body: JSON.generate(response_body_401), + ) + get(:show) + expect(response).to be_ok + end + it 'socure nil response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 401, + body: JSON.generate(no_doc_found_response_body), + ) + get(:show) + expect(response).to be_ok + end + end end describe '#update' do diff --git a/spec/services/doc_auth/socure/request_spec.rb b/spec/services/doc_auth/socure/request_spec.rb index feedb8318f8..10f2cc1fad1 100644 --- a/spec/services/doc_auth/socure/request_spec.rb +++ b/spec/services/doc_auth/socure/request_spec.rb @@ -10,7 +10,7 @@ end describe '#fetch' do - let(:fake_socure_endpoint) { 'https://fake-socure.com/' } + let(:fake_socure_endpoint) { 'https://fake-socure.test/' } let(:fake_metric_name) { 'fake metric' } before do @@ -40,8 +40,10 @@ let(:response) { nil } let(:response_status) { 403 } - it 'returns {}' do - expect(request.fetch).to eq({}) + # Because we have not implemented handle_connection_error at this level + # (defined in docv_result and document_request) + it 'raises a NotImplementedError' do + expect { request.fetch }.to raise_error NotImplementedError end end end diff --git a/spec/services/doc_auth/socure/requests/document_request_spec.rb b/spec/services/doc_auth/socure/requests/document_request_spec.rb index 46db7c5ed90..4ea862c903f 100644 --- a/spec/services/doc_auth/socure/requests/document_request_spec.rb +++ b/spec/services/doc_auth/socure/requests/document_request_spec.rb @@ -15,7 +15,7 @@ describe '#fetch' do let(:document_type) { 'license' } - let(:fake_socure_endpoint) { 'https://fake-socure.com/' } + let(:fake_socure_endpoint) { 'https://fake-socure.test/' } let(:fake_socure_document_capture_app_url) { 'https://verify.socure.us/something' } let(:docv_transaction_token) { 'fake docv transaction token' } let(:fake_socure_response) do @@ -99,5 +99,31 @@ expect { document_request.fetch }.not_to raise_error end end + context 'with timeout exception' do + let(:response) { nil } + let(:response_status) { 403 } + let(:faraday_connection_failed_exception) { Faraday::ConnectionFailed } + + before do + stub_request(:post, fake_socure_endpoint).to_raise(faraday_connection_failed_exception) + end + it 'expect handle_connection_error method to be called' do + connection_error_attributes = { + success: false, + errors: { network: true }, + exception: faraday_connection_failed_exception, + extra: { + vendor: 'Socure', + vendor_status_code: nil, + vendor_status_message: nil, + }.compact, + } + result = document_request.fetch + expect(result[:success]).to eq(connection_error_attributes[:success]) + expect(result[:errors]).to eq(connection_error_attributes[:errors]) + expect(result[:exception]).to be_a Faraday::ConnectionFailed + expect(result[:extra]).to eq(connection_error_attributes[:extra]) + end + end end end diff --git a/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb b/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb new file mode 100644 index 00000000000..2fe380431d8 --- /dev/null +++ b/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb @@ -0,0 +1,60 @@ +require 'rails_helper' + +RSpec.describe DocAuth::Socure::Requests::DocvResultRequest do + let(:document_capture_session_uuid) { 'fake uuid' } + let(:biometric_comparison_required) { false } + + subject(:docv_result_request) do + described_class.new( + document_capture_session_uuid:, + biometric_comparison_required: biometric_comparison_required, + ) + end + + describe '#fetch' do + let(:fake_socure_endpoint) { 'https://fake-socure.test/' } + let(:fake_socure_api_endpoint) { 'https://fake-socure.test/api/3.0/EmailAuthScore' } + let(:docv_transaction_token) { 'fake docv transaction token' } + let(:user) { create(:user) } + let(:document_capture_session) do + DocumentCaptureSession.create(user:).tap do |dcs| + dcs.socure_docv_transaction_token = docv_transaction_token + end + end + + before do + allow(IdentityConfig.store).to receive(:socure_idplus_base_url). + and_return(fake_socure_endpoint) + allow(DocumentCaptureSession).to receive(:find_by).and_return(document_capture_session) + end + + context 'with socure failures' do + let(:fake_socure_response) { {} } + let(:fake_socure_status) { 500 } + + it 'expect correct doc auth response during a connection failure' do + stub_request(:post, fake_socure_api_endpoint).to_raise(Faraday::ConnectionFailed) + response_hash = docv_result_request.fetch.to_h + expect(response_hash[:success]).to eq(false) + expect(response_hash[:errors]).to eq({ network: true }) + expect(response_hash[:vendor]).to eq('Socure') + expect(response_hash[:exception]).to be_a(Faraday::ConnectionFailed) + end + + it 'expect correct doc auth response for a socure fail response' do + stub_request(:post, fake_socure_api_endpoint). + to_return( + status: fake_socure_status, + body: JSON.generate(fake_socure_response), + ) + response_hash = docv_result_request.fetch.to_h + expect(response_hash[:success]).to eq(false) + expect(response_hash[:errors]).to eq({ network: true }) + expect(response_hash[:errors]).to eq({ network: true }) + expect(response_hash[:vendor]).to eq('Socure') + expect(response_hash[:exception]).to be_a(DocAuth::RequestError) + expect(response_hash[:exception].message).to include('Unexpected HTTP response 500') + end + end + end +end