From 7f619178f42792ba6e18f3f54e9b70854a92ecb3 Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Fri, 1 Mar 2024 15:32:43 -0500 Subject: [PATCH 01/13] document capture session result check if liveness is required [skip changelog] --- .../idv/capture_doc_status_controller.rb | 2 +- app/controllers/idv/document_capture_controller.rb | 7 ++++--- .../hybrid_mobile/document_capture_controller.rb | 9 +++++---- app/controllers/idv/link_sent_controller.rb | 3 ++- app/services/doc_auth/mock/result_response.rb | 14 +++++++------- app/services/document_capture_session_result.rb | 4 ++-- .../features/idv/doc_auth/document_capture_spec.rb | 11 +++++++++-- 7 files changed, 30 insertions(+), 20 deletions(-) diff --git a/app/controllers/idv/capture_doc_status_controller.rb b/app/controllers/idv/capture_doc_status_controller.rb index 4e69fb81765..9dd7bc000ca 100644 --- a/app/controllers/idv/capture_doc_status_controller.rb +++ b/app/controllers/idv/capture_doc_status_controller.rb @@ -33,7 +33,7 @@ def status elsif session_result.blank? || pending_barcode_attention_confirmation? || redo_document_capture_pending? :accepted - elsif !session_result.success? + elsif !session_result.success?(selfie_required: decorated_sp_session.selfie_required?) :unauthorized else :ok diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index 207630bda73..907f5dcb9e8 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -25,7 +25,7 @@ def update # Not used in standard flow, here for data consistency with hybrid flow. document_capture_session.confirm_ocr - result = handle_stored_result + form_response = handle_stored_result analytics.idv_doc_auth_document_capture_submitted(**result.to_h.merge(analytics_arguments)) Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]). @@ -33,7 +33,7 @@ def update cancel_establishing_in_person_enrollments - if result.success? + if form_response.success? redirect_to idv_ssn_url else redirect_to idv_document_capture_url @@ -98,7 +98,8 @@ def analytics_arguments end def handle_stored_result - if stored_result&.success? && selfie_requirement_met? + if stored_result&.success?(selfie_required: decorated_sp_session.selfie_required?) && + selfie_requirement_met? save_proofing_components(current_user) extract_pii_from_doc(current_user, store_in_session: true) flash[:success] = t('doc_auth.headings.capture_complete') diff --git a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb index 83892d2d038..137b3d28c5e 100644 --- a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb @@ -20,7 +20,7 @@ def show def update document_capture_session.confirm_ocr - result = handle_stored_result + form_response = handle_stored_result analytics.idv_doc_auth_document_capture_submitted(**result.to_h.merge(analytics_arguments)) @@ -28,7 +28,7 @@ def update call('document_capture', :update, true) # rate limiting redirect is in ImageUploadResponsePresenter - if result.success? + if form_response.success? flash[:success] = t('doc_auth.headings.capture_complete') redirect_to idv_hybrid_mobile_capture_complete_url else @@ -63,7 +63,8 @@ def analytics_arguments end def handle_stored_result - if stored_result&.success? && selfie_requirement_met? + if stored_result&.success?(selfie_required: decorated_sp_session.selfie_required?) && + selfie_requirement_met? save_proofing_components(document_capture_user) extract_pii_from_doc(document_capture_user) successful_response @@ -74,7 +75,7 @@ def handle_stored_result end def confirm_document_capture_needed - return unless stored_result&.success? + return unless stored_result&.success?(selfie_required: decorated_sp_session.selfie_required?) return if redo_document_capture_pending? redirect_to idv_hybrid_mobile_capture_complete_url diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index c99e007ac44..c59984a3f2b 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -80,7 +80,8 @@ def render_step_incomplete_error end def take_photo_with_phone_successful? - stored_result&.success? && selfie_requirement_met? + stored_result&.success?(selfie_required: decorated_sp_session.selfie_required?) && + selfie_requirement_met? end end end diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb index 4f1d388866a..e9e4938cec0 100644 --- a/app/services/doc_auth/mock/result_response.rb +++ b/app/services/doc_auth/mock/result_response.rb @@ -86,7 +86,7 @@ def pii_from_doc end def success? - doc_auth_success? && (selfie_check_performed? ? selfie_passed? : true) + doc_auth_success? && (@selfie_required ? selfie_passed? : true) end def attention_with_barcode? @@ -142,12 +142,12 @@ def doc_auth_success? end def selfie_status - if @selfie_required - return :success if portrait_match_results&.dig(:FaceMatchResult).nil? - portrait_match_results[:FaceMatchResult] == 'Pass' ? :success : :fail - else - :not_processed + if portrait_match_results&.dig(:FaceMatchResult).nil? + return :success if @selfie_required + return :not_processed end + + portrait_match_results[:FaceMatchResult] == 'Pass' ? :success : :fail end def workflow @@ -264,4 +264,4 @@ def failed_file_data(failed_alerts_data) end end end -end +end \ No newline at end of file diff --git a/app/services/document_capture_session_result.rb b/app/services/document_capture_session_result.rb index 33ae3c94e94..1a8374366ef 100644 --- a/app/services/document_capture_session_result.rb +++ b/app/services/document_capture_session_result.rb @@ -26,9 +26,9 @@ def selfie_status self[:selfie_status].to_sym end - def success_status + def success_status(selfie_required:) # doc_auth_success : including document, attention_with_barcode and id type verification - !!doc_auth_success && selfie_status != :fail && !!pii + !!doc_auth_success && (selfie_required ? selfie_status == :success : true) && !!pii end alias_method :success?, :success_status diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index 84e677d9c2f..c0b501e2348 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -192,11 +192,18 @@ expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) - attach_and_submit_images + # doc auth is successful while liveness is not req'd + attach_images( + Rails.root.join( + 'spec', 'fixtures', + 'ial2_test_credential_no_liveness.yml' + ), + ) + submit_images expect(page).to have_current_path(idv_ssn_url) expect_costing_for_document - expect(DocAuthLog.find_by(user_id: user.id).state).to eq('MT') + expect(DocAuthLog.find_by(user_id: user.id).state).to eq('NY') expect(page).to have_current_path(idv_ssn_url) fill_out_ssn_form_ok From e99953145d47690f5b69919f35d191e9ab7c31f1 Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Fri, 1 Mar 2024 16:04:27 -0500 Subject: [PATCH 02/13] rename result to form_response --- app/controllers/idv/document_capture_controller.rb | 2 +- .../idv/hybrid_mobile/document_capture_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index 907f5dcb9e8..a9e971c4372 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -26,7 +26,7 @@ def update document_capture_session.confirm_ocr form_response = handle_stored_result - analytics.idv_doc_auth_document_capture_submitted(**result.to_h.merge(analytics_arguments)) + analytics.idv_doc_auth_document_capture_submitted(**form_response.to_h.merge(analytics_arguments)) Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]). call('document_capture', :update, true) diff --git a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb index 137b3d28c5e..0cf9b556637 100644 --- a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb @@ -22,7 +22,7 @@ def update document_capture_session.confirm_ocr form_response = handle_stored_result - analytics.idv_doc_auth_document_capture_submitted(**result.to_h.merge(analytics_arguments)) + analytics.idv_doc_auth_document_capture_submitted(**form_response.to_h.merge(analytics_arguments)) Funnel::DocAuth::RegisterStep.new(document_capture_user.id, sp_session[:issuer]). call('document_capture', :update, true) From 49e3f9940c6a741263417232a2a60f66839c884f Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Fri, 1 Mar 2024 16:10:54 -0500 Subject: [PATCH 03/13] do not store selfie result if liveness checking is disabled --- .../lexis_nexis/responses/true_id_response.rb | 2 +- .../responses/true_id_response_spec.rb | 193 +++++++++++------- 2 files changed, 117 insertions(+), 78 deletions(-) diff --git a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb index b8c64a9f8d6..4b9d121269a 100644 --- a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb +++ b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb @@ -108,7 +108,7 @@ def billed? # return :success if selfie check result == 'Pass' # return :fail def selfie_status - return :not_processed if selfie_result.nil? + return :not_processed if selfie_result.nil? || !@liveness_checking_enabled selfie_result == 'Pass' ? :success : :fail end diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb index 40479f87e60..061c6f91e96 100644 --- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb @@ -9,6 +9,9 @@ let(:success_with_liveness_response) do instance_double(Faraday::Response, status: 200, body: LexisNexisFixtures.true_id_response_success_with_liveness) end + let(:doc_auth_success_with_face_match_fail) do + instance_double(Faraday::Response, status: 200, body: LexisNexisFixtures.true_id_response_with_face_match_fail) + end let(:failure_response_face_match_fail) do instance_double(Faraday::Response, status: 200, body: LexisNexisFixtures.true_id_response_with_face_match_fail) end @@ -66,6 +69,40 @@ expect(response.successful_result?).to eq(true) expect(response.to_h[:vendor]).to eq('TrueID') end + + context 'when a portrait match is returned' do + let(:liveness_enabled) { true } + context 'when selfie status is failed' do + let(:response) do + described_class.new(doc_auth_success_with_face_match_fail, config, liveness_enabled, request_context) + end + it 'is a failed result' do + expect(response.selfie_status).to eq(:fail) + expect(response.success?).to eq(false) + expect(response.to_h[:vendor]).to eq('TrueID') + end + + context 'when a liveness check was not requested' do + let(:liveness_enabled) { false } + it 'is a successful result' do + expect(response.selfie_status).to eq(:not_processed) + expect(response.success?).to eq(true) + expect(response.to_h[:vendor]).to eq('TrueID') + end + end + end + context 'when selfie status passes' do + let(:response) do + described_class.new(success_with_liveness_response, config, liveness_enabled, request_context) + end + it 'is a successful result' do + expect(response.selfie_status).to eq(:success) + expect(response.success?).to eq(true) + expect(response.to_h[:vendor]).to eq('TrueID') + end + end + end + it 'has no error messages' do expect(response.error_messages).to be_empty end @@ -325,84 +362,86 @@ def get_decision_product(resp) expect(errors[:back]).to contain_exactly(DocAuth::Errors::FALLBACK_FIELD_LEVEL) expect(errors[:hints]).to eq(true) end + context 'when liveness enabled' do + let(:liveness_enabled) { true } + it 'returns Failed for visible_pattern when it gets passed and failed value ' do + output = described_class.new(failure_response_no_liveness, config, liveness_enabled).to_h + expect(output.to_h[:log_alert_results]). + to match(a_hash_including(visible_pattern: { no_side: 'Failed' })) + end - it 'returns Failed for visible_pattern when it gets passed and failed value ' do - output = described_class.new(failure_response_no_liveness, config).to_h - expect(output.to_h[:log_alert_results]). - to match(a_hash_including(visible_pattern: { no_side: 'Failed' })) - end - - it 'returns Failed for liveness failure' do - response = described_class.new(failure_response_with_liveness, config) - output = response.to_h - expect(output[:success]).to eq(false) - expect(response.doc_auth_success?).to eq(false) - expect(response.selfie_status).to eq(:fail) - end - - it 'produces expected hash output' do - output = described_class.new( - failure_response_with_all_failures, config, liveness_enabled, - request_context - ).to_h + it 'returns Failed for liveness failure' do + response = described_class.new(failure_response_with_liveness, config, liveness_enabled) + output = response.to_h + expect(output[:success]).to eq(false) + expect(response.doc_auth_success?).to eq(false) + expect(response.selfie_status).to eq(:fail) + end - expect(output).to match( - success: false, - exception: nil, - errors: { - general: [DocAuth::Errors::GENERAL_ERROR], - front: [DocAuth::Errors::FALLBACK_FIELD_LEVEL], - back: [DocAuth::Errors::FALLBACK_FIELD_LEVEL], - hints: true, - }, - attention_with_barcode: false, - doc_type_supported: true, - conversation_id: a_kind_of(String), - request_id: a_kind_of(String), - reference: a_kind_of(String), - vendor: 'TrueID', - billed: true, - log_alert_results: a_hash_including('2d_barcode_content': { no_side: 'Failed' }), - transaction_status: 'failed', - transaction_reason_code: 'failed_true_id', - product_status: 'pass', - decision_product_status: 'fail', - doc_auth_result: 'Failed', - processed_alerts: a_hash_including(:passed, :failed), - address_line2_present: false, - alert_failure_count: a_kind_of(Numeric), - portrait_match_results: { - 'FaceMatchResult' => 'Fail', - 'FaceMatchScore' => '0', - 'FaceStatusCode' => '0', - 'FaceErrorMessage' => 'Liveness: PoorQuality', - }, - image_metrics: a_hash_including(:front, :back), - 'ClassificationMode' => 'Automatic', - 'DocAuthResult' => 'Failed', - 'DocClass' => 'DriversLicense', - 'DocClassCode' => 'DriversLicense', - 'DocClassName' => 'Drivers License', - 'DocumentName' => 'Connecticut (CT) Driver License', - 'DocIssuerCode' => 'CT', - 'DocIssuerType' => 'StateProvince', - 'DocIssuerName' => 'Connecticut', - 'DocIssue' => '2009', - 'DocIssueType' => 'Driver License', - 'DocIsGeneric' => 'false', - 'OrientationChanged' => 'false', - 'PresentationChanged' => 'false', - classification_info: { - Front: a_hash_including(:ClassName, :CountryCode, :IssuerType), - Back: a_hash_including(:ClassName, :CountryCode, :IssuerType), - }, - doc_auth_success: false, - selfie_status: :fail, - selfie_live: true, - selfie_quality_good: false, - liveness_enabled: false, - workflow: anything, - ) + it 'produces expected hash output' do + output = described_class.new( + failure_response_with_all_failures, config, liveness_enabled, + request_context + ).to_h + + expect(output).to match( + success: false, + exception: nil, + errors: { + general: [DocAuth::Errors::GENERAL_ERROR], + front: [DocAuth::Errors::FALLBACK_FIELD_LEVEL], + back: [DocAuth::Errors::FALLBACK_FIELD_LEVEL], + hints: true, + }, + attention_with_barcode: false, + doc_type_supported: true, + conversation_id: a_kind_of(String), + request_id: a_kind_of(String), + reference: a_kind_of(String), + vendor: 'TrueID', + billed: true, + log_alert_results: a_hash_including('2d_barcode_content': { no_side: 'Failed' }), + transaction_status: 'failed', + transaction_reason_code: 'failed_true_id', + product_status: 'pass', + decision_product_status: 'fail', + doc_auth_result: 'Failed', + processed_alerts: a_hash_including(:passed, :failed), + address_line2_present: false, + alert_failure_count: a_kind_of(Numeric), + portrait_match_results: { + 'FaceMatchResult' => 'Fail', + 'FaceMatchScore' => '0', + 'FaceStatusCode' => '0', + 'FaceErrorMessage' => 'Liveness: PoorQuality', + }, + image_metrics: a_hash_including(:front, :back), + 'ClassificationMode' => 'Automatic', + 'DocAuthResult' => 'Failed', + 'DocClass' => 'DriversLicense', + 'DocClassCode' => 'DriversLicense', + 'DocClassName' => 'Drivers License', + 'DocumentName' => 'Connecticut (CT) Driver License', + 'DocIssuerCode' => 'CT', + 'DocIssuerType' => 'StateProvince', + 'DocIssuerName' => 'Connecticut', + 'DocIssue' => '2009', + 'DocIssueType' => 'Driver License', + 'DocIsGeneric' => 'false', + 'OrientationChanged' => 'false', + 'PresentationChanged' => 'false', + classification_info: { + Front: a_hash_including(:ClassName, :CountryCode, :IssuerType), + Back: a_hash_including(:ClassName, :CountryCode, :IssuerType), + }, + doc_auth_success: false, + selfie_status: :fail, + selfie_live: true, + selfie_quality_good: false, + liveness_enabled: true, + workflow: anything, + ) + end end it 'produces appropriate errors with document tampering' do output = described_class.new(failure_response_tampering, config).to_h @@ -780,4 +819,4 @@ def get_decision_product(resp) end end end -end +end \ No newline at end of file From 3769854c67a947d93eb58e5c478a887b949c556d Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Mon, 4 Mar 2024 13:37:58 -0500 Subject: [PATCH 04/13] update spec to use selfie_req'd param --- .../idv/image_uploads_controller_spec.rb | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index 157393aa063..ee436844033 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -8,6 +8,7 @@ let(:back_image) { DocAuthImageFixtures.document_back_image_multipart } let(:selfie_img) { nil } let(:state_id_number) { 'S59397998' } + let(:liveness_checking_required) { false } describe '#create' do subject(:action) do @@ -354,6 +355,7 @@ context 'selfie included' do let(:back_image) { DocAuthImageFixtures.portrait_match_success_yaml } let(:selfie_img) { DocAuthImageFixtures.selfie_image_multipart } + let(:liveness_checking_required) { true } before do allow(controller.decorated_sp_session).to receive(:selfie_required?).and_return(true) @@ -368,7 +370,7 @@ image_source: :unknown, user_uuid: an_instance_of(String), uuid_prefix: nil, - liveness_checking_required: true, + liveness_checking_required: liveness_checking_required, images_cropped: false, ).and_call_original @@ -376,7 +378,11 @@ expect(response.status).to eq(200) expect(json[:success]).to eq(true) - expect(document_capture_session.reload.load_result.success?).to eq(true) + expect( + document_capture_session.reload.load_result.success?( + selfie_required: liveness_checking_required, + ), + ).to eq(true) expect(document_capture_session.reload.load_result.selfie_check_performed?).to eq(true) end end @@ -390,7 +396,7 @@ image_source: :unknown, user_uuid: an_instance_of(String), uuid_prefix: nil, - liveness_checking_required: false, + liveness_checking_required: liveness_checking_required, images_cropped: false, ).and_call_original @@ -398,7 +404,10 @@ expect(response.status).to eq(200) expect(json[:success]).to eq(true) - expect(document_capture_session.reload.load_result.success?).to eq(true) + expect( + document_capture_session.reload.load_result. + success?(selfie_required: liveness_checking_required), + ).to eq(true) end it 'tracks events' do @@ -1271,12 +1280,17 @@ let(:back_image) { DocAuthImageFixtures.portrait_match_success_yaml } let(:selfie_img) { DocAuthImageFixtures.selfie_image_multipart } + let(:liveness_checking_required) { true } it 'returns a successful response' do action expect(response.status).to eq(200) expect(json[:success]).to eq(true) - expect(document_capture_session.reload.load_result.success?).to eq(true) + expect( + document_capture_session.reload.load_result.success?( + selfie_required: liveness_checking_required, + ), + ).to eq(true) expect(document_capture_session.reload.load_result.selfie_check_performed?).to eq(true) end @@ -1296,7 +1310,11 @@ action expect(response.status).to eq(200) expect(json[:success]).to eq(true) - expect(document_capture_session.reload.load_result.success?).to eq(true) + expect( + document_capture_session.reload.load_result.success?( + selfie_required: liveness_checking_required, + ), + ).to eq(true) end end end From ba7ca96d457b8fc9a8185655622ded5222f16ea2 Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Mon, 4 Mar 2024 14:53:27 -0500 Subject: [PATCH 05/13] update docuement capture session result tests to evaluate selfie_status basedon whether a selfie is required --- .../document_capture_session_result_spec.rb | 144 ++++++++++++------ 1 file changed, 96 insertions(+), 48 deletions(-) diff --git a/spec/services/document_capture_session_result_spec.rb b/spec/services/document_capture_session_result_spec.rb index b32434f5ee7..76720fac1c2 100644 --- a/spec/services/document_capture_session_result_spec.rb +++ b/spec/services/document_capture_session_result_spec.rb @@ -4,27 +4,9 @@ let(:id) { SecureRandom.uuid } let(:success) { true } let(:pii) { { 'first_name' => 'Testy', 'last_name' => 'Testerson' } } + let(:selfie_required) { false } context 'EncryptedRedisStructStorage' do - it 'works with EncryptedRedisStructStorage' do - result = DocumentCaptureSessionResult.new( - id: id, - success: success, - doc_auth_success: success, - selfie_status: :success, - pii: pii, - attention_with_barcode: false, - ) - EncryptedRedisStructStorage.store(result) - loaded_result = EncryptedRedisStructStorage.load(id, type: DocumentCaptureSessionResult) - - expect(loaded_result.id).to eq(id) - expect(loaded_result.success?).to eq(success) - expect(loaded_result.pii).to eq(pii.deep_symbolize_keys) - expect(loaded_result.attention_with_barcode?).to eq(false) - expect(loaded_result.selfie_status).to eq(:success) - expect(loaded_result.doc_auth_success).to eq(true) - end it 'add fingerprint with EncryptedRedisStructStorage' do result = DocumentCaptureSessionResult.new( id: id, @@ -39,6 +21,29 @@ expect(result.failed_front_image?(nil)).to eq(false) expect(result.failed_back_image?(nil)).to eq(false) end + context 'with selfie' do + let(:selfie_required) { true } + it 'works with EncryptedRedisStructStorage' do + result = DocumentCaptureSessionResult.new( + id: id, + success: success, + doc_auth_success: success, + selfie_status: :success, + pii: pii, + attention_with_barcode: false, + ) + EncryptedRedisStructStorage.store(result) + loaded_result = EncryptedRedisStructStorage.load(id, type: DocumentCaptureSessionResult) + + expect(loaded_result.id).to eq(id) + expect(loaded_result.success?(selfie_required: selfie_required)).to eq(success) + expect(loaded_result.pii).to eq(pii.deep_symbolize_keys) + expect(loaded_result.attention_with_barcode?).to eq(false) + expect(loaded_result.selfie_status).to eq(:success) + expect(loaded_result.doc_auth_success).to eq(true) + end + end + describe '#selfie_status' do it 'returns a symbol' do result = DocumentCaptureSessionResult.new( @@ -62,7 +67,7 @@ selfie_status: :not_processed, doc_auth_success: true, ) - expect(result.success?).to eq(true) + expect(result.success?(selfie_required: selfie_required)).to eq(true) end it 'reports correctly from false when missing doc_auth_success and selfie_status' do result = DocumentCaptureSessionResult.new( @@ -71,44 +76,51 @@ pii: pii, attention_with_barcode: false, ) - expect(result.success?).to eq(false) + expect(result.success?(selfie_required: selfie_required)).to eq(false) end - it 'reports failure when selfie_status is :fail' do - result = DocumentCaptureSessionResult.new( - id: id, - success: false, - pii: pii, - attention_with_barcode: false, - selfie_status: :fail, - doc_auth_success: true, - ) - expect(result.success?).to eq(false) + context 'when pii is not present' do + it 'reports success status is failed' do + result = DocumentCaptureSessionResult.new( + id: id, + success: true, + pii: nil, + attention_with_barcode: false, + doc_auth_success: true, + selfie_status: :not_processed, + ) + expect(result.success?(selfie_required: selfie_required)).to eq(false) + end end - - it 'reports failure when doc_auth_success is false' do - result = DocumentCaptureSessionResult.new( - id: id, - success: false, - pii: pii, - attention_with_barcode: false, - selfie_status: :success, - doc_auth_success: false, - ) - expect(result.success?).to eq(false) + context 'when selfie status is failed' do + it 'reports success status is successful' do + result = DocumentCaptureSessionResult.new( + id: id, + success: false, + pii: pii, + attention_with_barcode: false, + doc_auth_success: true, + selfie_status: :fail, + ) + expect(result.success?(selfie_required: selfie_required)).to eq(true) + end end - - describe 'when success field, doc_auth_success, and selfie_status conflict' do - it 'reports correct result' do + context 'when selfie status is success' do + it 'reports success status is successful' do result = DocumentCaptureSessionResult.new( id: id, success: false, pii: pii, attention_with_barcode: false, - selfie_status: :not_processed, doc_auth_success: true, + selfie_status: :success, ) - expect(result.success?).to eq(true) + expect(result.success?(selfie_required: selfie_required)).to eq(true) + end + end + context 'when a liveness check is required' do + let(:selfie_required) { true } + it 'reports failure when selfie_status is :fail' do result = DocumentCaptureSessionResult.new( id: id, success: true, @@ -117,7 +129,43 @@ selfie_status: :fail, doc_auth_success: true, ) - expect(result.success?).to eq(false) + expect(result.success?(selfie_required: selfie_required)).to eq(false) + end + + it 'reports failure when selfie_status is not_processed' do + result = DocumentCaptureSessionResult.new( + id: id, + success: true, + pii: pii, + attention_with_barcode: false, + selfie_status: :not_processed, + doc_auth_success: true, + ) + expect(result.success?(selfie_required: selfie_required)).to eq(false) + end + + it 'reports failure when doc_auth_success is false' do + result = DocumentCaptureSessionResult.new( + id: id, + success: true, + pii: pii, + attention_with_barcode: false, + selfie_status: :success, + doc_auth_success: false, + ) + expect(result.success?(selfie_required: selfie_required)).to eq(false) + end + + it 'reports failure when selfie_status is not_processed' do + result = DocumentCaptureSessionResult.new( + id: id, + success: false, + pii: pii, + attention_with_barcode: false, + selfie_status: :success, + doc_auth_success: true, + ) + expect(result.success?(selfie_required: selfie_required)).to eq(true) end end end From 1645661e234e3a8ef93481466fcb0db8b0387024 Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Mon, 4 Mar 2024 14:58:29 -0500 Subject: [PATCH 06/13] add selfie_required param --- spec/models/document_capture_session_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/document_capture_session_spec.rb b/spec/models/document_capture_session_spec.rb index 07b4ac0d08c..736e3a001ac 100644 --- a/spec/models/document_capture_session_spec.rb +++ b/spec/models/document_capture_session_spec.rb @@ -54,7 +54,7 @@ record.store_result_from_response(doc_auth_response) result = record.load_result - expect(result.success?).to eq(doc_auth_response.success?) + expect(result.success?(selfie_required: true)).to eq(doc_auth_response.success?) expect(result.pii).to eq(doc_auth_response.pii_from_doc.deep_symbolize_keys) end From d38248e54327b8bc3d3d7f20a1d3f618826ccd40 Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Mon, 4 Mar 2024 15:17:30 -0500 Subject: [PATCH 07/13] happy linting --- app/services/doc_auth/mock/result_response.rb | 2 +- app/services/doc_auth/selfie_concern.rb | 4 ++-- .../lexis_nexis/responses/true_id_response_spec.rb | 12 +++++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb index e9e4938cec0..dab42d8ee81 100644 --- a/app/services/doc_auth/mock/result_response.rb +++ b/app/services/doc_auth/mock/result_response.rb @@ -264,4 +264,4 @@ def failed_file_data(failed_alerts_data) end end end -end \ No newline at end of file +end diff --git a/app/services/doc_auth/selfie_concern.rb b/app/services/doc_auth/selfie_concern.rb index 89e562168fd..dc58f04ee16 100644 --- a/app/services/doc_auth/selfie_concern.rb +++ b/app/services/doc_auth/selfie_concern.rb @@ -29,7 +29,7 @@ def selfie_check_performed? SELFIE_PERFORMED_STATUSES.include?(selfie_status) end - private + private SELFIE_PERFORMED_STATUSES = %i[success fail] @@ -43,5 +43,5 @@ def selfie_check_performed? def get_portrait_error(portrait_match_results) portrait_match_results&.with_indifferent_access&.dig(:FaceErrorMessage) end -end + end end diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb index 061c6f91e96..27fcf3b6988 100644 --- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb @@ -74,7 +74,10 @@ let(:liveness_enabled) { true } context 'when selfie status is failed' do let(:response) do - described_class.new(doc_auth_success_with_face_match_fail, config, liveness_enabled, request_context) + described_class.new( + doc_auth_success_with_face_match_fail, config, liveness_enabled, + request_context + ) end it 'is a failed result' do expect(response.selfie_status).to eq(:fail) @@ -93,7 +96,10 @@ end context 'when selfie status passes' do let(:response) do - described_class.new(success_with_liveness_response, config, liveness_enabled, request_context) + described_class.new( + success_with_liveness_response, config, liveness_enabled, + request_context + ) end it 'is a successful result' do expect(response.selfie_status).to eq(:success) @@ -819,4 +825,4 @@ def get_decision_product(resp) end end end -end \ No newline at end of file +end From e1693e842d040caeb9c292000b04eb0d012d6b8d Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Mon, 4 Mar 2024 15:48:25 -0500 Subject: [PATCH 08/13] test that selfie without a status failes when liveness check is required --- .../responses/true_id_response_spec.rb | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb index 27fcf3b6988..21f540d2226 100644 --- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb @@ -746,13 +746,14 @@ def get_decision_product(resp) end context 'when selfie check is enabled' do + let(:liveness_checking_enabled) { true } context 'whe missing selfie result in response' do let(:request_context) do { workflow: 'selfie_workflow', } end - let(:response) { described_class.new(success_response, config, true, request_context) } + let(:response) { described_class.new(success_response, config, liveness_checking_enabled, request_context) } it 'returns :not_processed when missing selfie in response' do expect(response.selfie_status).to eq(:not_processed) end @@ -763,13 +764,13 @@ def get_decision_product(resp) end end context 'when selfie passed' do - let(:response) { described_class.new(success_with_liveness_response, config, true) } + let(:response) { described_class.new(success_with_liveness_response, config, liveness_checking_enabled) } it 'returns :success' do expect(response.selfie_status).to eq(:success) end end context 'when selfie failed' do - let(:response) { described_class.new(failure_response_with_liveness, config, true) } + let(:response) { described_class.new(failure_response_with_liveness, config, liveness_checking_enabled) } it 'returns :fail' do expect(response.selfie_status).to eq(:fail) end @@ -778,12 +779,12 @@ def get_decision_product(resp) end describe '#successful_result?' do - context 'and selfie check is enabled' do - let(:liveness_checking_enabled) { true } + context 'and liveness check is enabled' do + let(:liveness_enabled) { true } it 'returns true with a passing selfie' do response = described_class.new( - success_with_liveness_response, config, liveness_checking_enabled + success_with_liveness_response, config, liveness_enabled ) expect(response.successful_result?).to eq(true) @@ -791,7 +792,7 @@ def get_decision_product(resp) context 'when portrait match fails' do it 'returns false with a failing selfie' do response = described_class.new( - failure_response_face_match_fail, config, liveness_checking_enabled + failure_response_face_match_fail, config, liveness_enabled ) expect(response.successful_result?).to eq(false) @@ -801,7 +802,7 @@ def get_decision_product(resp) described_class.new( attention_barcode_read_with_face_match_fail, config, - liveness_checking_enabled, + liveness_enabled, ) end @@ -810,6 +811,19 @@ def get_decision_product(resp) expect(response.successful_result?).to eq(false) end end + + context 'when a portrait match result is not returned' do + let(:response) do + described_class.new( + success_response, config, liveness_enabled + ) + end + + it 'returns false' do + expect(response.selfie_status).to eq(:not_processed) + expect(response.successful_result?).to eq(false) + end + end end context 'and selfie check is disabled' do From 10135feae9ea066a01ee7ee9090e5e2f676f3dc5 Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Mon, 4 Mar 2024 16:10:51 -0500 Subject: [PATCH 09/13] rename liveness_enabled to liveness_checking_enabled consistent with TrueIDReponse signature --- .../responses/true_id_response_spec.rb | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb index 21f540d2226..7a81f7bf870 100644 --- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb @@ -53,7 +53,7 @@ let(:config) do DocAuth::LexisNexis::Config.new end - let(:liveness_enabled) { false } + let(:liveness_checking_enabled) { false } let(:workflow) { 'default_workflow' } let(:request_context) do { @@ -62,7 +62,7 @@ end context 'when the response is a success' do let(:response) do - described_class.new(success_response, config, liveness_enabled, request_context) + described_class.new(success_response, config, liveness_checking_enabled, request_context) end it 'is a successful result' do @@ -71,11 +71,11 @@ end context 'when a portrait match is returned' do - let(:liveness_enabled) { true } + let(:liveness_checking_enabled) { true } context 'when selfie status is failed' do let(:response) do described_class.new( - doc_auth_success_with_face_match_fail, config, liveness_enabled, + doc_auth_success_with_face_match_fail, config, liveness_checking_enabled, request_context ) end @@ -86,7 +86,7 @@ end context 'when a liveness check was not requested' do - let(:liveness_enabled) { false } + let(:liveness_checking_enabled) { false } it 'is a successful result' do expect(response.selfie_status).to eq(:not_processed) expect(response.success?).to eq(true) @@ -97,7 +97,7 @@ context 'when selfie status passes' do let(:response) do described_class.new( - success_with_liveness_response, config, liveness_enabled, + success_with_liveness_response, config, liveness_checking_enabled, request_context ) end @@ -369,15 +369,15 @@ def get_decision_product(resp) expect(errors[:hints]).to eq(true) end context 'when liveness enabled' do - let(:liveness_enabled) { true } + let(:liveness_checking_enabled) { true } it 'returns Failed for visible_pattern when it gets passed and failed value ' do - output = described_class.new(failure_response_no_liveness, config, liveness_enabled).to_h + output = described_class.new(failure_response_no_liveness, config, liveness_checking_enabled).to_h expect(output.to_h[:log_alert_results]). to match(a_hash_including(visible_pattern: { no_side: 'Failed' })) end it 'returns Failed for liveness failure' do - response = described_class.new(failure_response_with_liveness, config, liveness_enabled) + response = described_class.new(failure_response_with_liveness, config, liveness_checking_enabled) output = response.to_h expect(output[:success]).to eq(false) expect(response.doc_auth_success?).to eq(false) @@ -386,7 +386,7 @@ def get_decision_product(resp) it 'produces expected hash output' do output = described_class.new( - failure_response_with_all_failures, config, liveness_enabled, + failure_response_with_all_failures, config, liveness_checking_enabled, request_context ).to_h @@ -485,7 +485,7 @@ def get_decision_product(resp) it 'produces reasonable output for a TrueID failure without details' do output = described_class.new( - failure_response_empty, config, liveness_enabled, + failure_response_empty, config, liveness_checking_enabled, request_context ).to_h @@ -502,7 +502,7 @@ def get_decision_product(resp) it 'produces reasonable output for a malformed TrueID response' do allow(NewRelic::Agent).to receive(:notice_error) output = described_class.new( - failure_response_malformed, config, liveness_enabled, + failure_response_malformed, config, liveness_checking_enabled, request_context ).to_h @@ -780,11 +780,11 @@ def get_decision_product(resp) describe '#successful_result?' do context 'and liveness check is enabled' do - let(:liveness_enabled) { true } + let(:liveness_checking_enabled) { true } it 'returns true with a passing selfie' do response = described_class.new( - success_with_liveness_response, config, liveness_enabled + success_with_liveness_response, config, liveness_checking_enabled ) expect(response.successful_result?).to eq(true) @@ -792,7 +792,7 @@ def get_decision_product(resp) context 'when portrait match fails' do it 'returns false with a failing selfie' do response = described_class.new( - failure_response_face_match_fail, config, liveness_enabled + failure_response_face_match_fail, config, liveness_checking_enabled ) expect(response.successful_result?).to eq(false) @@ -802,7 +802,7 @@ def get_decision_product(resp) described_class.new( attention_barcode_read_with_face_match_fail, config, - liveness_enabled, + liveness_checking_enabled, ) end @@ -815,7 +815,7 @@ def get_decision_product(resp) context 'when a portrait match result is not returned' do let(:response) do described_class.new( - success_response, config, liveness_enabled + success_response, config, liveness_checking_enabled ) end From 1b0202b543b178dd9cea25e86f6c9e2fbcab29a1 Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Mon, 4 Mar 2024 16:14:34 -0500 Subject: [PATCH 10/13] happy linting --- .../idv/document_capture_controller.rb | 4 +++- .../document_capture_controller.rb | 7 ++++-- .../responses/true_id_response_spec.rb | 22 ++++++++++++++----- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index a9e971c4372..1ebf750a44e 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -26,7 +26,9 @@ def update document_capture_session.confirm_ocr form_response = handle_stored_result - analytics.idv_doc_auth_document_capture_submitted(**form_response.to_h.merge(analytics_arguments)) + analytics.idv_doc_auth_document_capture_submitted( + **form_response.to_h.merge(analytics_arguments), + ) Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]). call('document_capture', :update, true) diff --git a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb index 0cf9b556637..72471772a41 100644 --- a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb @@ -22,7 +22,9 @@ def update document_capture_session.confirm_ocr form_response = handle_stored_result - analytics.idv_doc_auth_document_capture_submitted(**form_response.to_h.merge(analytics_arguments)) + analytics.idv_doc_auth_document_capture_submitted( + **form_response.to_h.merge(analytics_arguments), + ) Funnel::DocAuth::RegisterStep.new(document_capture_user.id, sp_session[:issuer]). call('document_capture', :update, true) @@ -75,7 +77,8 @@ def handle_stored_result end def confirm_document_capture_needed - return unless stored_result&.success?(selfie_required: decorated_sp_session.selfie_required?) + return unless stored_result&. + success?(selfie_required: decorated_sp_session.selfie_required?) return if redo_document_capture_pending? redirect_to idv_hybrid_mobile_capture_complete_url diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb index 7a81f7bf870..1828ba098f5 100644 --- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb @@ -371,13 +371,19 @@ def get_decision_product(resp) context 'when liveness enabled' do let(:liveness_checking_enabled) { true } it 'returns Failed for visible_pattern when it gets passed and failed value ' do - output = described_class.new(failure_response_no_liveness, config, liveness_checking_enabled).to_h + output = described_class.new( + failure_response_no_liveness, config, + liveness_checking_enabled + ).to_h expect(output.to_h[:log_alert_results]). to match(a_hash_including(visible_pattern: { no_side: 'Failed' })) end it 'returns Failed for liveness failure' do - response = described_class.new(failure_response_with_liveness, config, liveness_checking_enabled) + response = described_class.new( + failure_response_with_liveness, config, + liveness_checking_enabled + ) output = response.to_h expect(output[:success]).to eq(false) expect(response.doc_auth_success?).to eq(false) @@ -753,7 +759,9 @@ def get_decision_product(resp) workflow: 'selfie_workflow', } end - let(:response) { described_class.new(success_response, config, liveness_checking_enabled, request_context) } + let(:response) do + described_class.new(success_response, config, liveness_checking_enabled, request_context) + end it 'returns :not_processed when missing selfie in response' do expect(response.selfie_status).to eq(:not_processed) end @@ -764,13 +772,17 @@ def get_decision_product(resp) end end context 'when selfie passed' do - let(:response) { described_class.new(success_with_liveness_response, config, liveness_checking_enabled) } + let(:response) do + described_class.new(success_with_liveness_response, config, liveness_checking_enabled) + end it 'returns :success' do expect(response.selfie_status).to eq(:success) end end context 'when selfie failed' do - let(:response) { described_class.new(failure_response_with_liveness, config, liveness_checking_enabled) } + let(:response) do + described_class.new(failure_response_with_liveness, config, liveness_checking_enabled) + end it 'returns :fail' do expect(response.selfie_status).to eq(:fail) end From 8920b82a867eac27e3b892827aa1801d22da27f3 Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Mon, 4 Mar 2024 16:30:10 -0500 Subject: [PATCH 11/13] undo DocAuth::Mock#selfie_status changes made to recreate the bug --- app/services/doc_auth/mock/result_response.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb index dab42d8ee81..ce2e153ce6d 100644 --- a/app/services/doc_auth/mock/result_response.rb +++ b/app/services/doc_auth/mock/result_response.rb @@ -142,9 +142,11 @@ def doc_auth_success? end def selfie_status - if portrait_match_results&.dig(:FaceMatchResult).nil? - return :success if @selfie_required - return :not_processed + if @selfie_required + return :success if portrait_match_results&.dig(:FaceMatchResult).nil? + portrait_match_results[:FaceMatchResult] == 'Pass' ? :success : :fail + else + :not_processed end portrait_match_results[:FaceMatchResult] == 'Pass' ? :success : :fail From efd8015bafb8da2d94859759bb90345c103a3dbc Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Tue, 5 Mar 2024 10:51:43 -0500 Subject: [PATCH 12/13] undo mock change --- app/services/doc_auth/mock/result_response.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb index ce2e153ce6d..480a9b8622b 100644 --- a/app/services/doc_auth/mock/result_response.rb +++ b/app/services/doc_auth/mock/result_response.rb @@ -148,8 +148,6 @@ def selfie_status else :not_processed end - - portrait_match_results[:FaceMatchResult] == 'Pass' ? :success : :fail end def workflow From cefc72aa1bcf3c12668b4afbe574daaeecb06b8c Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Wed, 6 Mar 2024 10:53:00 -0500 Subject: [PATCH 13/13] update selfie_status comment --- .../doc_auth/lexis_nexis/responses/true_id_response.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb index 4b9d121269a..41bf6a71016 100644 --- a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb +++ b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb @@ -103,7 +103,8 @@ def billed? end # @return [:success, :fail, :not_processed] - # When selfie result is missing, return :not_processed + # When selfie result is missing or not requested: + # return :not_processed # Otherwise: # return :success if selfie check result == 'Pass' # return :fail