From 63ebf90c543aaf77745b1470e9f6b0f030c26c4a Mon Sep 17 00:00:00 2001 From: Dawei Wang Date: Fri, 19 Jan 2024 13:59:44 -0500 Subject: [PATCH 1/7] LG-11718: test from client perspective with selfie enabled. --- .../lexis_nexis/lexis_nexis_client_spec.rb | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb b/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb index db8afa1c1f6..82e37523b0c 100644 --- a/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb @@ -17,6 +17,8 @@ trueid_account_id: 'test_account', trueid_noliveness_cropping_workflow: 'NOLIVENESS.CROPPING.WORKFLOW', trueid_noliveness_nocropping_workflow: 'NOLIVENESS.NOCROPPING.WORKFLOW', + trueid_liveness_cropping_workflow: 'LIVENESS.CROPPING.WORKFLOW', + trueid_liveness_nocropping_workflow: 'LIVENESS.NOCROPPING.WORKFLOW', ) end @@ -117,4 +119,73 @@ ) end end + + context 'when selfie check enabled' do + describe 'when success response returned' do + before do + stub_request(:post, image_upload_url).to_return( + body: LexisNexisFixtures.true_id_response_success_with_liveness, + ) + end + it 'returns a successful response' do + result = client.post_images( + front_image: DocAuthImageFixtures.document_front_image, + back_image: DocAuthImageFixtures.document_back_image, + image_source: image_source, + selfie_image: DocAuthImageFixtures.selfie_image, + liveness_checking_required: true, + ) + expect(result.success?).to eq(true) + expect(result.class).to eq(DocAuth::LexisNexis::Responses::TrueIdResponse) + expect(result.doc_auth_success?).to eq(true) + expect(result.selfie_success).to eq(true) + end + end + + describe 'when selfie failure response returned' do + before do + stub_request(:post, image_upload_url).to_return( + body: LexisNexisFixtures.true_id_response_failure_with_liveness, + ) + end + + it 'returns a response indicate all failures' do + result = client.post_images( + front_image: DocAuthImageFixtures.document_front_image, + back_image: DocAuthImageFixtures.document_back_image, + image_source: image_source, + selfie_image: DocAuthImageFixtures.selfie_image, + liveness_checking_required: true, + ) + expect(result.success?).to eq(false) + expect(result.class).to eq(DocAuth::LexisNexis::Responses::TrueIdResponse) + expect(result.doc_auth_success?).to eq(false) + expect(result.selfie_success).to eq(false) + end + end + + describe 'when http request failed' do + it 'return failed response with correct statuses' do + stub_request(:post, image_upload_url).to_return(body: '', status: 401) + + result = client.post_images( + front_image: DocAuthImageFixtures.document_front_image, + back_image: DocAuthImageFixtures.document_back_image, + image_source: image_source, + selfie_image: DocAuthImageFixtures.selfie_image, + liveness_checking_required: true, + ) + + expect(result.success?).to eq(false) + expect(result.errors).to eq(network: true) + expect(result.exception.message).to eq( + 'DocAuth::LexisNexis::Requests::TrueIdRequest Unexpected HTTP response 401', + ) + expect(result.selfie_success).to eq(nil) + result_hash = result.to_h + expect(result_hash[:vendor]).to eq('TrueID') + expect(result.class).to eq(DocAuth::Response) + end + end + end end From 96185fe405a67973efd2e7a5505ba43390dd57b6 Mon Sep 17 00:00:00 2001 From: Dawei Wang Date: Mon, 22 Jan 2024 11:28:27 -0500 Subject: [PATCH 2/7] LG-11718: mock dependency not self with api_image_upload_form_spec. Other minor changes. --- spec/forms/idv/api_image_upload_form_spec.rb | 32 ++++++++++++++++++- .../lexis_nexis/lexis_nexis_client_spec.rb | 2 +- .../doc_auth/lexis_nexis/request_spec.rb | 7 ++-- .../requests/true_id_request_spec.rb | 4 +-- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index cfd292b4090..e4ab18be451 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -268,8 +268,10 @@ extra: { remaining_attempts: IdentityConfig.store.doc_auth_max_attempts - 1 }, ) end + let(:doc_auth_client) { double(DocAuth::LexisNexis::LexisNexisClient) } before do - allow(subject).to receive(:post_images_to_client).and_return(failed_response) + subject.instance_variable_set(:@doc_auth_client, doc_auth_client) + allow(doc_auth_client).to receive(:post_images) { failed_response } end it 'is not successful' do @@ -279,6 +281,7 @@ expect(response.success?).to eq(false) expect(response.attention_with_barcode?).to eq(false) expect(response.pii_from_doc).to eq({}) + expect(response.doc_auth_success?).to eq(false) end it 'includes remaining_attempts' do @@ -300,6 +303,33 @@ expect(response.errors).to have_key(:front) expect(response.errors).to have_value([I18n.t('doc_auth.errors.doc.resubmit_failed_image')]) end + + it 'logs analytics event' do + form.submit + expect(fake_analytics).to have_logged_event( + 'IdV: doc auth image upload vendor submitted', + async: false, + attempts: 1, + attention_with_barcode: false, + billed: nil, + client_image_metrics: an_instance_of(Hash), + doc_auth_result: nil, + errors: { front: 'glare' }, + exception: nil, + flow_path: anything, + remaining_attempts: 3, + state: nil, + state_id_type: nil, + success: false, + user_id: document_capture_session.user.uuid, + vendor_request_time_in_ms: a_kind_of(Float), + front_image_fingerprint: an_instance_of(String), + back_image_fingerprint: an_instance_of(String), + doc_type_supported: boolean, + doc_auth_success: boolean, + selfie_success: nil, + ) + end end context 'PII validation from client response fails' do diff --git a/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb b/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb index 82e37523b0c..2a4c92ac22e 100644 --- a/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb @@ -120,7 +120,7 @@ end end - context 'when selfie check enabled' do + context 'with selfie check enabled' do describe 'when success response returned' do before do stub_request(:post, image_upload_url).to_return( diff --git a/spec/services/doc_auth/lexis_nexis/request_spec.rb b/spec/services/doc_auth/lexis_nexis/request_spec.rb index b108dde4e1a..ee1deb71aab 100644 --- a/spec/services/doc_auth/lexis_nexis/request_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/request_spec.rb @@ -88,12 +88,15 @@ end it 'includes information on the error' do - response = subject.fetch expected_message = [ subject.class.name, 'Unexpected HTTP response', - status, + 500, ].join(' ') + expect(NewRelic::Agent).to receive(:notice_error).with( + DocAuth::RequestError.new(expected_message, 500), + ) + response = subject.fetch expect(response.exception.message).to eq(expected_message) end diff --git a/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb b/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb index b16b387df7d..46c050d3577 100644 --- a/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb @@ -196,8 +196,8 @@ def include_liveness_expected response = subject.fetch expect(response.network_error?).to eq(true) end - it 'is not a network error with 440, 438, 439' do - stub_request(:post, full_url).to_return(body: '{}', status: 443) + it 'is non 500 error' do + stub_request(:post, full_url).to_return(body: '{}', status: 401) response = subject.fetch expect(response.network_error?).to eq(true) end From d023ea1f058bd5dc9b04547d05b01fc84db18710 Mon Sep 17 00:00:00 2001 From: Dawei Wang Date: Mon, 22 Jan 2024 13:34:32 -0500 Subject: [PATCH 3/7] LG-11718: update feature test to verify error message. --- .../idv/doc_auth/redo_document_capture_spec.rb | 4 ++++ spec/services/doc_auth/lexis_nexis/request_spec.rb | 12 +++++++----- .../lexis_nexis/requests/true_id_request_spec.rb | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/spec/features/idv/doc_auth/redo_document_capture_spec.rb b/spec/features/idv/doc_auth/redo_document_capture_spec.rb index 2bd63a927cf..2aa297a36a6 100644 --- a/spec/features/idv/doc_auth/redo_document_capture_spec.rb +++ b/spec/features/idv/doc_auth/redo_document_capture_spec.rb @@ -186,6 +186,8 @@ complete_doc_auth_steps_before_document_capture_step mock_doc_auth_trueid_http_non2xx_status(438) attach_and_submit_images + # verify it's a network error + expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) click_try_again end @@ -199,6 +201,8 @@ complete_doc_auth_steps_before_document_capture_step mock_doc_auth_trueid_http_non2xx_status(500) attach_and_submit_images + # verify it's a network error + expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) click_try_again end it_behaves_like 'image re-upload allowed' diff --git a/spec/services/doc_auth/lexis_nexis/request_spec.rb b/spec/services/doc_auth/lexis_nexis/request_spec.rb index ee1deb71aab..5ed3f7c9fe4 100644 --- a/spec/services/doc_auth/lexis_nexis/request_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/request_spec.rb @@ -87,15 +87,17 @@ expect(response.class).to eq(DocAuth::Response) end - it 'includes information on the error' do + it 'includes information on the error and notifies NewRelic' do expected_message = [ subject.class.name, 'Unexpected HTTP response', - 500, + status, ].join(' ') - expect(NewRelic::Agent).to receive(:notice_error).with( - DocAuth::RequestError.new(expected_message, 500), - ) + expect(NewRelic::Agent).to receive(:notice_error) do |arg| + expect(arg).to be_an_instance_of(DocAuth::RequestError) + expect(arg.message).to eq(expected_message) + expect(arg.error_code).to eq(status) + end response = subject.fetch expect(response.exception.message).to eq(expected_message) diff --git a/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb b/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb index 46c050d3577..cf1ab8b594b 100644 --- a/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb @@ -196,7 +196,7 @@ def include_liveness_expected response = subject.fetch expect(response.network_error?).to eq(true) end - it 'is non 500 error' do + it 'is a network error with non 5xx error' do stub_request(:post, full_url).to_return(body: '{}', status: 401) response = subject.fetch expect(response.network_error?).to eq(true) From a03d80f2934a0d58d87d0c232c28438aa5457317 Mon Sep 17 00:00:00 2001 From: Dawei Wang Date: Mon, 22 Jan 2024 16:36:07 -0500 Subject: [PATCH 4/7] LG-11718: update to use workflow lookup. --- .../requests/true_id_request_spec.rb | 63 ++++++++++++------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb b/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb index cf1ab8b594b..56f97ea60e3 100644 --- a/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb @@ -3,19 +3,24 @@ RSpec.describe DocAuth::LexisNexis::Requests::TrueIdRequest do let(:image_source) { nil } let(:account_id) { 'test_account' } - let(:workflow) { nil } let(:base_url) { 'https://lexis.nexis.example.com' } + let(:workflow) { subject.send(:workflow) } let(:path) { "/restws/identity/v3/accounts/#{account_id}/workflows/#{workflow}/conversations" } let(:full_url) { base_url + path } let(:applicant) { { uuid: SecureRandom.uuid, uuid_prefix: '123' } } + let(:non_cropping_non_liveness_flow) { 'test_workflow' } + let(:cropping_non_liveness_flow) { 'test_workflow_cropping' } + let(:non_cropping_liveness_flow) { 'test_workflow_liveness' } + let(:cropping_liveness_flow) { 'test_workflow_liveness_cropping' } + let(:config) do DocAuth::LexisNexis::Config.new( trueid_account_id: account_id, base_url: base_url, - trueid_noliveness_cropping_workflow: 'test_workflow_cropping', - trueid_noliveness_nocropping_workflow: 'test_workflow', - trueid_liveness_cropping_workflow: 'test_workflow_liveness_cropping', - trueid_liveness_nocropping_workflow: 'test_workflow_liveness', + trueid_noliveness_cropping_workflow: cropping_non_liveness_flow, + trueid_noliveness_nocropping_workflow: non_cropping_non_liveness_flow, + trueid_liveness_cropping_workflow: cropping_liveness_flow, + trueid_liveness_nocropping_workflow: non_cropping_liveness_flow, ) end let(:selfie_image) { DocAuthImageFixtures.selfie_image } @@ -100,10 +105,11 @@ def include_liveness_expected context 'when liveness checking is NOT required' do let(:liveness_checking_required) { false } context 'with acuant image source' do - let(:workflow) { 'test_workflow' } let(:image_source) { DocAuth::ImageSources::ACUANT_SDK } it_behaves_like 'a successful request' - + it 'uses non-cropping non-liveness workflow' do + expect(subject.send(:workflow)).to eq(non_cropping_non_liveness_flow) + end it 'does not include a nil selfie in the request body sent to TrueID' do body_as_json = subject.send(:body) body_as_hash = JSON.parse(body_as_json) @@ -111,9 +117,10 @@ def include_liveness_expected end end context 'with unknown image source' do - let(:workflow) { 'test_workflow_cropping' } let(:image_source) { DocAuth::ImageSources::UNKNOWN } - + it 'uses cropping non-liveness workflow' do + expect(subject.send(:workflow)).to eq(cropping_non_liveness_flow) + end it_behaves_like 'a successful request' end end @@ -121,14 +128,17 @@ def include_liveness_expected context 'when liveness checking is required' do let(:liveness_checking_required) { true } context 'with acuant image source' do - let(:workflow) { 'test_workflow' } let(:image_source) { DocAuth::ImageSources::ACUANT_SDK } + it 'uses non-cropping non-liveness workflow' do + expect(subject.send(:workflow)).to eq(non_cropping_non_liveness_flow) + end it_behaves_like 'a successful request' end context 'with unknown image source' do - let(:workflow) { 'test_workflow_cropping' } let(:image_source) { DocAuth::ImageSources::UNKNOWN } - + it 'uses cropping non-liveness workflow' do + expect(subject.send(:workflow)).to eq(cropping_non_liveness_flow) + end it_behaves_like 'a successful request' end end @@ -144,14 +154,17 @@ def include_liveness_expected context 'when liveness checking is NOT required' do let(:liveness_checking_required) { false } context 'with acuant image source' do - let(:workflow) { 'test_workflow' } let(:image_source) { DocAuth::ImageSources::ACUANT_SDK } + it 'use non-cropping non-liveness workflow' do + expect(subject.send(:workflow)).to eq(non_cropping_non_liveness_flow) + end it_behaves_like 'a successful request' end context 'with unknown image source' do - let(:workflow) { 'test_workflow_cropping' } let(:image_source) { DocAuth::ImageSources::UNKNOWN } - + it 'use cropping non-liveness workflow' do + expect(subject.send(:workflow)).to eq(cropping_non_liveness_flow) + end it_behaves_like 'a successful request' end end @@ -159,29 +172,34 @@ def include_liveness_expected context 'when liveness checking is required' do let(:liveness_checking_required) { true } context 'with acuant image source' do - let(:workflow) { 'test_workflow_liveness' } let(:image_source) { DocAuth::ImageSources::ACUANT_SDK } - + it 'use non-cropping liveness workflow' do + expect(subject.send(:workflow)).to eq(non_cropping_liveness_flow) + end it_behaves_like 'a successful request' end context 'with unknown image source' do - let(:workflow) { 'test_workflow_liveness_cropping' } let(:image_source) { DocAuth::ImageSources::UNKNOWN } - + it 'use cropping liveness workflow' do + expect(subject.send(:workflow)).to eq(cropping_liveness_flow) + end it_behaves_like 'a successful request' end context 'when hosted env is prod' do let(:selfie_check_allowed) { false } context 'with acuant image source' do - let(:workflow) { 'test_workflow' } let(:image_source) { DocAuth::ImageSources::ACUANT_SDK } + it 'use non-cropping non-liveness workflow' do + expect(subject.send(:workflow)).to eq(non_cropping_non_liveness_flow) + end it_behaves_like 'a successful request' end context 'with unknown image source' do - let(:workflow) { 'test_workflow_cropping' } let(:image_source) { DocAuth::ImageSources::UNKNOWN } - + it 'use cropping non-liveness workflow' do + expect(subject.send(:workflow)).to eq(cropping_non_liveness_flow) + end it_behaves_like 'a successful request' end end @@ -189,7 +207,6 @@ def include_liveness_expected end context 'with non 200 http status code' do - let(:workflow) { 'test_workflow' } let(:image_source) { DocAuth::ImageSources::ACUANT_SDK } it 'is a network error with 5xx status' do stub_request(:post, full_url).to_return(body: '{}', status: 500) From a290133d7e445beec174a24240096e6a5c84d600 Mon Sep 17 00:00:00 2001 From: Dawei Wang Date: Thu, 25 Jan 2024 09:34:45 -0500 Subject: [PATCH 5/7] LG-11718: bring in changes from main branch and consolidate. --- app/services/doc_auth/lexis_nexis/request.rb | 6 +++++- spec/forms/idv/api_image_upload_form_spec.rb | 5 ++++- .../lexis_nexis/lexis_nexis_client_spec.rb | 18 +++++++++++++++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/services/doc_auth/lexis_nexis/request.rb b/app/services/doc_auth/lexis_nexis/request.rb index ec3dad05f80..99961c3e452 100644 --- a/app/services/doc_auth/lexis_nexis/request.rb +++ b/app/services/doc_auth/lexis_nexis/request.rb @@ -54,7 +54,11 @@ def handle_connection_error(exception) success: false, errors: { network: true }, exception: exception, - extra: { vendor: 'TrueID' }, + extra: { + vendor: 'TrueID', + selfie_live: false, + selfie_quality_good: false, + }, ) end diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index e4ab18be451..107607ec0bf 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -327,7 +327,10 @@ back_image_fingerprint: an_instance_of(String), doc_type_supported: boolean, doc_auth_success: boolean, - selfie_success: nil, + liveness_checking_required: boolean, + selfie_status: :not_processed, + selfie_live: boolean, + selfie_quality_good: boolean, ) end end diff --git a/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb b/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb index 2a4c92ac22e..692179b5306 100644 --- a/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb @@ -121,6 +121,11 @@ end context 'with selfie check enabled' do + ## enable feature + let(:workflow) { 'LIVENESS.CROPPING.WORKFLOW' } + before do + allow(FeatureManagement).to receive(:idv_allow_selfie_check?).and_return(true) + end describe 'when success response returned' do before do stub_request(:post, image_upload_url).to_return( @@ -138,7 +143,9 @@ expect(result.success?).to eq(true) expect(result.class).to eq(DocAuth::LexisNexis::Responses::TrueIdResponse) expect(result.doc_auth_success?).to eq(true) - expect(result.selfie_success).to eq(true) + expect(result.selfie_live?).to eq(true) + expect(result.selfie_quality_good?).to eq(true) + expect(result.selfie_check_performed?).to eq(true) end end @@ -160,7 +167,11 @@ expect(result.success?).to eq(false) expect(result.class).to eq(DocAuth::LexisNexis::Responses::TrueIdResponse) expect(result.doc_auth_success?).to eq(false) - expect(result.selfie_success).to eq(false) + result_hash = result.to_h + expect(result_hash[:selfie_status]).to eq(:fail) + expect(result.selfie_live?).to eq(true) + expect(result.selfie_quality_good?).to eq(false) + expect(result.selfie_check_performed?).to eq(true) end end @@ -181,9 +192,10 @@ expect(result.exception.message).to eq( 'DocAuth::LexisNexis::Requests::TrueIdRequest Unexpected HTTP response 401', ) - expect(result.selfie_success).to eq(nil) result_hash = result.to_h expect(result_hash[:vendor]).to eq('TrueID') + expect(result_hash[:doc_auth_success]).to eq(false) + expect(result_hash[:selfie_status]).to eq(:not_processed) expect(result.class).to eq(DocAuth::Response) end end From 618c2be5c86730ae5ffde1a6dbf8f4d2f4a58981 Mon Sep 17 00:00:00 2001 From: Dawei Wang Date: Thu, 25 Jan 2024 09:50:55 -0500 Subject: [PATCH 6/7] changelog: Internal, Doc Auth, Validating vendor http error situation From 0b624ff9d73165d1e3955e980df22cc4c6bbccb5 Mon Sep 17 00:00:00 2001 From: Dawei Wang Date: Tue, 30 Jan 2024 08:47:18 -0500 Subject: [PATCH 7/7] LG-11718: minor update from comment. --- spec/forms/idv/api_image_upload_form_spec.rb | 34 +++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index 107607ec0bf..63bf9a575da 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -308,29 +308,17 @@ form.submit expect(fake_analytics).to have_logged_event( 'IdV: doc auth image upload vendor submitted', - async: false, - attempts: 1, - attention_with_barcode: false, - billed: nil, - client_image_metrics: an_instance_of(Hash), - doc_auth_result: nil, - errors: { front: 'glare' }, - exception: nil, - flow_path: anything, - remaining_attempts: 3, - state: nil, - state_id_type: nil, - success: false, - user_id: document_capture_session.user.uuid, - vendor_request_time_in_ms: a_kind_of(Float), - front_image_fingerprint: an_instance_of(String), - back_image_fingerprint: an_instance_of(String), - doc_type_supported: boolean, - doc_auth_success: boolean, - liveness_checking_required: boolean, - selfie_status: :not_processed, - selfie_live: boolean, - selfie_quality_good: boolean, + hash_including( + doc_auth_result: nil, + errors: { front: 'glare' }, + success: false, + doc_type_supported: boolean, + doc_auth_success: boolean, + liveness_checking_required: boolean, + selfie_status: :not_processed, + selfie_live: boolean, + selfie_quality_good: boolean, + ), ) end end