From 9aea0977de509c470279801dfc135c0ae4f0f4c8 Mon Sep 17 00:00:00 2001
From: Amir Reavis-Bey
Date: Wed, 31 Jan 2024 13:48:09 -0500
Subject: [PATCH 01/25] LG-12041: Prevent user from resubmitting a selfie that
already failed portrait matching (#9976)
* init add selfie to failed images
* rebase merge conflict resolved
* remove comments
* rebase merge conflict resolved
* resolve failing tests
* happy linting
changelog: Upcoming Features, Document Authentication, Store fingerprint of failed selfie images and prevent user from reusing same image
* add selfie to mocked response
* test #store_failed_auth_data with selfie
* remove guar dto store back/front fingerprints if doc auth is successful b/ failure could be doc_pii
* remove irrelavant test
* add feature test for resubmitting same failed selfie during doc auth
* update comment
* add sleep to allow warning to appear
* fail selie validation to test selfie resubmisssion
* feature test doc auth vs portrait match pass/fail scenarios and ability to reload a failed image
* add liveness_enabled
* remove unneeded comment
* test storing failed images when pii validation fails
* remove selfie status helper function
* remove selfie_status_from_response helper
* DocAuthResponse instance double to stub selfie_status
* remove comma typo
---
app/forms/idv/api_image_upload_form.rb | 61 +-
app/helpers/application_helper.rb | 6 -
app/models/document_capture_session.rb | 13 +-
.../image_upload_response_presenter.rb | 2 +-
app/services/analytics_events.rb | 6 +-
.../lexis_nexis/responses/true_id_response.rb | 2 +-
app/services/doc_auth/response.rb | 7 +-
.../document_capture_session_result.rb | 12 +-
.../idv/image_uploads_controller_spec.rb | 32 +-
spec/features/idv/analytics_spec.rb | 18 +-
.../doc_auth/redo_document_capture_spec.rb | 205 ++++-
.../ial2_test_portrait_match_failure.yml | 17 +
.../ial2_test_portrait_match_success.yml | 16 +
...response_failure_with_face_match_pass.json | 740 ++++++++++++++++++
spec/forms/idv/api_image_upload_form_spec.rb | 193 ++++-
spec/helpers/application_helper_spec.rb | 23 -
spec/models/document_capture_session_spec.rb | 50 +-
.../image_upload_response_presenter_spec.rb | 20 +-
.../responses/true_id_response_spec.rb | 2 +
spec/support/doc_auth_image_fixtures.rb | 16 +
spec/support/features/doc_auth_helper.rb | 64 ++
spec/support/lexis_nexis_fixtures.rb | 4 +
22 files changed, 1414 insertions(+), 95 deletions(-)
create mode 100644 spec/fixtures/ial2_test_portrait_match_failure.yml
create mode 100644 spec/fixtures/ial2_test_portrait_match_success.yml
create mode 100644 spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_with_face_match_pass.json
diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb
index 37399c27cbd..7227ac29c2e 100644
--- a/app/forms/idv/api_image_upload_form.rb
+++ b/app/forms/idv/api_image_upload_form.rb
@@ -155,6 +155,7 @@ def extra_attributes
@extra_attributes[:front_image_fingerprint] = front_image_fingerprint
@extra_attributes[:back_image_fingerprint] = back_image_fingerprint
+ @extra_attributes[:selfie_image_fingerprint] = selfie_image_fingerprint
@extra_attributes[:liveness_checking_required] = liveness_checking_required
@extra_attributes
end
@@ -175,6 +176,16 @@ def back_image_fingerprint
end
end
+ def selfie_image_fingerprint
+ return unless liveness_checking_required
+ return @selfie_image_fingerprint if @selfie_image_fingerprint
+
+ if readable?(:selfie)
+ @selfie_image_fingerprint =
+ Digest::SHA256.urlsafe_base64digest(selfie_image_bytes)
+ end
+ end
+
def remaining_attempts
rate_limiter.remaining_count if document_capture_session
end
@@ -262,6 +273,15 @@ def validate_duplicate_images
side: error_sides.length == 2 ? 'both' : error_sides[0], **extra_attributes,
)
end
+
+ if capture_result&.failed_selfie_image?(selfie_image_fingerprint)
+ errors.add(
+ :selfie, t('doc_auth.errors.doc.resubmit_failed_image'), type: :duplicate_image
+ )
+ analytics.idv_doc_auth_failed_image_resubmitted(
+ side: 'selfie', **extra_attributes,
+ )
+ end
end
def limit_if_rate_limited
@@ -439,35 +459,39 @@ def store_failed_images(client_response, doc_pii_response)
return {
front: [],
back: [],
+ selfie: [],
}
end
# doc auth failed due to non network error or doc_pii is not valid
if client_response && !client_response.success? && !client_response.network_error?
errors_hash = client_response.errors&.to_h || {}
- ## assume both sides' error presents or both sides' error missing
- failed_front_fingerprint = extra_attributes[:front_image_fingerprint]
- failed_back_fingerprint = extra_attributes[:back_image_fingerprint]
- ## not both sides' error present nor both sides' error missing
- ## equivalent to: only one side error presents
- only_one_side_error = errors_hash[:front]&.present? ^ errors_hash[:back]&.present?
- if only_one_side_error
- ## find which side is missing
- failed_front_fingerprint = nil unless errors_hash[:front]&.present?
- failed_back_fingerprint = nil unless errors_hash[:back]&.present?
+ failed_front_fingerprint = nil
+ failed_back_fingerprint = nil
+ if errors_hash[:front] || errors_hash[:back]
+ if errors_hash[:front]
+ failed_front_fingerprint = extra_attributes[:front_image_fingerprint]
+ end
+ if errors_hash[:back]
+ failed_back_fingerprint = extra_attributes[:back_image_fingerprint]
+ end
+ elsif !client_response.doc_auth_success?
+ failed_front_fingerprint = extra_attributes[:front_image_fingerprint]
+ failed_back_fingerprint = extra_attributes[:back_image_fingerprint]
end
- document_capture_session.
- store_failed_auth_data(
- front_image_fingerprint: failed_front_fingerprint,
- back_image_fingerprint: failed_back_fingerprint,
- doc_auth_success: client_response.doc_auth_success?,
- selfie_status: selfie_status_from_response(client_response),
- )
+ document_capture_session.store_failed_auth_data(
+ front_image_fingerprint: failed_front_fingerprint,
+ back_image_fingerprint: failed_back_fingerprint,
+ selfie_image_fingerprint: extra_attributes[:selfie_image_fingerprint],
+ doc_auth_success: client_response.doc_auth_success?,
+ selfie_status: client_response.selfie_status,
+ )
elsif doc_pii_response && !doc_pii_response.success?
document_capture_session.store_failed_auth_data(
front_image_fingerprint: extra_attributes[:front_image_fingerprint],
back_image_fingerprint: extra_attributes[:back_image_fingerprint],
+ selfie_image_fingerprint: extra_attributes[:selfie_image_fingerprint],
doc_auth_success: client_response.doc_auth_success?,
- selfie_status: selfie_status_from_response(client_response),
+ selfie_status: client_response.selfie_status,
)
end
# retrieve updated data from session
@@ -475,6 +499,7 @@ def store_failed_images(client_response, doc_pii_response)
{
front: captured_result&.failed_front_image_fingerprints || [],
back: captured_result&.failed_back_image_fingerprints || [],
+ selfie: captured_result&.failed_selfie_image_fingerprints || [],
}
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 3e434470734..7b0eb6c382a 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -60,10 +60,4 @@ def cancel_link_text
def desktop_device?
!BrowserCache.parse(request.user_agent).mobile?
end
-
- def selfie_status_from_response(client_response)
- return client_response.selfie_status if client_response.respond_to?(:selfie_status)
-
- :not_processed
- end
end
diff --git a/app/models/document_capture_session.rb b/app/models/document_capture_session.rb
index dd36db9aae5..94ca97fe105 100644
--- a/app/models/document_capture_session.rb
+++ b/app/models/document_capture_session.rb
@@ -19,7 +19,7 @@ def store_result_from_response(doc_auth_response)
session_result.attention_with_barcode = doc_auth_response.attention_with_barcode?
session_result.selfie_check_performed = doc_auth_response.selfie_check_performed?
session_result.doc_auth_success = doc_auth_response.doc_auth_success?
- session_result.selfie_status = selfie_status_from_response(doc_auth_response)
+ session_result.selfie_status = doc_auth_response.selfie_status
EncryptedRedisStructStorage.store(
session_result,
expires_in: IdentityConfig.store.doc_capture_request_valid_for_minutes.minutes.seconds.to_i,
@@ -28,8 +28,8 @@ def store_result_from_response(doc_auth_response)
save!
end
- def store_failed_auth_data(front_image_fingerprint:, back_image_fingerprint:, doc_auth_success:,
- selfie_status:)
+ def store_failed_auth_data(front_image_fingerprint:, back_image_fingerprint:,
+ selfie_image_fingerprint:, doc_auth_success:, selfie_status:)
session_result = load_result || DocumentCaptureSessionResult.new(
id: generate_result_id,
)
@@ -37,8 +37,11 @@ def store_failed_auth_data(front_image_fingerprint:, back_image_fingerprint:, do
session_result.captured_at = Time.zone.now
session_result.doc_auth_success = doc_auth_success
session_result.selfie_status = selfie_status
- session_result.add_failed_front_image!(front_image_fingerprint) if front_image_fingerprint
- session_result.add_failed_back_image!(back_image_fingerprint) if back_image_fingerprint
+
+ session_result.add_failed_front_image!(front_image_fingerprint)
+ session_result.add_failed_back_image!(back_image_fingerprint)
+ session_result.add_failed_selfie_image!(selfie_image_fingerprint) if selfie_status == :fail
+
EncryptedRedisStructStorage.store(
session_result,
expires_in: IdentityConfig.store.doc_capture_request_valid_for_minutes.minutes.seconds.to_i,
diff --git a/app/presenters/image_upload_response_presenter.rb b/app/presenters/image_upload_response_presenter.rb
index cc1b8d9f7ca..3078377f680 100644
--- a/app/presenters/image_upload_response_presenter.rb
+++ b/app/presenters/image_upload_response_presenter.rb
@@ -89,7 +89,7 @@ def doc_type_supported?
end
def failed_fingerprints
- @form_response.extra[:failed_image_fingerprints] || { front: [], back: [] }
+ @form_response.extra[:failed_image_fingerprints] || { front: [], back: [], selfie: [] }
end
def show_selfie_failures
diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb
index eff2e191771..d97eb786034 100644
--- a/app/services/analytics_events.rb
+++ b/app/services/analytics_events.rb
@@ -1018,7 +1018,7 @@ def idv_doc_auth_submitted_image_upload_form(
# @param [Boolean] attention_with_barcode
# @param [Boolean] doc_type_supported
# @param [Boolean] doc_auth_success
- # @param [Boolean] selfie_success
+ # @param [String] selfie_status
# @param [String] vendor
# @param [String] conversation_id
# @param [String] reference
@@ -1066,7 +1066,7 @@ def idv_doc_auth_submitted_image_upload_vendor(
attention_with_barcode: nil,
doc_type_supported: nil,
doc_auth_success: nil,
- selfie_success: nil,
+ selfie_status: nil,
vendor: nil,
conversation_id: nil,
reference: nil,
@@ -1102,7 +1102,7 @@ def idv_doc_auth_submitted_image_upload_vendor(
attention_with_barcode:,
doc_type_supported:,
doc_auth_success:,
- selfie_success:,
+ selfie_status:,
vendor:,
conversation_id:,
reference:,
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 d117fa72582..9c3b96748c0 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
@@ -221,7 +221,6 @@ def response_info
def create_response_info
alerts = parsed_alerts
log_alert_formatter = DocAuth::ProcessedAlertToLogAlertFormatter.new
-
{
transaction_status: transaction_status,
transaction_reason_code: transaction_reason_code,
@@ -235,6 +234,7 @@ def create_response_info
image_metrics: parse_image_metrics,
address_line2_present: !pii_from_doc[:address2].blank?,
classification_info: classification_info,
+ liveness_enabled: @liveness_checking_enabled,
}
end
diff --git a/app/services/doc_auth/response.rb b/app/services/doc_auth/response.rb
index d5e7506688c..9be626d4ce1 100644
--- a/app/services/doc_auth/response.rb
+++ b/app/services/doc_auth/response.rb
@@ -76,7 +76,7 @@ def to_h
selfie_live: selfie_live?,
selfie_quality_good: selfie_quality_good?,
doc_auth_success: doc_auth_success?,
- selfie_status: selfie_status_from_response(self),
+ selfie_status: selfie_status,
}.merge(extra)
end
@@ -103,5 +103,10 @@ def doc_auth_success?
# to be implemented by concrete subclass
false
end
+
+ def selfie_status
+ # to be implemented by concrete subclass
+ :not_processed
+ end
end
end
diff --git a/app/services/document_capture_session_result.rb b/app/services/document_capture_session_result.rb
index 3a8c527afee..5d09e275242 100644
--- a/app/services/document_capture_session_result.rb
+++ b/app/services/document_capture_session_result.rb
@@ -8,13 +8,15 @@
:attention_with_barcode,
:failed_front_image_fingerprints,
:failed_back_image_fingerprints,
+ :failed_selfie_image_fingerprints,
:captured_at,
:selfie_check_performed,
:doc_auth_success, :selfie_status, :selfie_success,
keyword_init: true,
allowed_members: [:id, :success, :attention_with_barcode, :failed_front_image_fingerprints,
- :failed_back_image_fingerprints, :captured_at, :selfie_check_performed,
- :doc_auth_success, :selfie_status, :selfie_success]
+ :failed_back_image_fingerprints, :failed_selfie_image_fingerprints,
+ :captured_at, :selfie_check_performed, :doc_auth_success, :selfie_status,
+ :selfie_success]
) do
def self.redis_key_prefix
'dcs:result'
@@ -28,11 +30,13 @@ def selfie_status
alias_method :attention_with_barcode?, :attention_with_barcode
alias_method :pii_from_doc, :pii
- %w[front back].each do |side|
+ %w[front back selfie].each do |side|
define_method(:"add_failed_#{side}_image!") do |fingerprint|
member_name = "failed_#{side}_image_fingerprints"
self[member_name] ||= []
- self[member_name] << fingerprint
+ if fingerprint && !self[member_name].include?(fingerprint)
+ self[member_name] << fingerprint
+ end
end
define_method(:"failed_#{side}_image?") do |fingerprint|
diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb
index 4ee86c00895..f0ab25f4dad 100644
--- a/spec/controllers/idv/image_uploads_controller_spec.rb
+++ b/spec/controllers/idv/image_uploads_controller_spec.rb
@@ -132,6 +132,7 @@
flow_path: 'standard',
front_image_fingerprint: nil,
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
)
@@ -203,7 +204,7 @@
result_failed: false,
ocr_pii: nil,
doc_type_supported: true,
- failed_image_fingerprints: { front: [], back: [] },
+ failed_image_fingerprints: { front: [], back: [], selfie: [] },
},
)
end
@@ -219,7 +220,7 @@
result_failed: false,
ocr_pii: nil,
doc_type_supported: true,
- failed_image_fingerprints: { front: [], back: [] },
+ failed_image_fingerprints: { front: [], back: [], selfie: [] },
}
end
@@ -267,6 +268,7 @@
flow_path: 'standard',
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
)
@@ -410,6 +412,7 @@
flow_path: 'standard',
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
)
@@ -436,6 +439,7 @@
vendor_request_time_in_ms: a_kind_of(Float),
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
doc_type_supported: boolean,
doc_auth_success: boolean,
selfie_status: :not_processed,
@@ -452,7 +456,6 @@
processed_alerts: nil,
product_status: nil,
reference: nil,
- selfie_success: nil,
transaction_reason_code: nil,
transaction_status: nil,
vendor: nil,
@@ -470,6 +473,7 @@
flow_path: 'standard',
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
classification_info: a_kind_of(Hash),
)
@@ -607,6 +611,7 @@
flow_path: 'standard',
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
)
@@ -633,6 +638,7 @@
vendor_request_time_in_ms: a_kind_of(Float),
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
doc_type_supported: boolean,
doc_auth_success: boolean,
selfie_status: :not_processed,
@@ -649,7 +655,6 @@
processed_alerts: nil,
product_status: nil,
reference: nil,
- selfie_success: nil,
transaction_reason_code: nil,
transaction_status: nil,
vendor: nil,
@@ -672,6 +677,7 @@
flow_path: 'standard',
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
classification_info: hash_including(
Front: hash_including(ClassName: 'Identification Card', CountryCode: 'USA'),
@@ -717,6 +723,7 @@
flow_path: 'standard',
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
)
@@ -743,6 +750,7 @@
vendor_request_time_in_ms: a_kind_of(Float),
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
doc_type_supported: boolean,
doc_auth_success: boolean,
selfie_status: :not_processed,
@@ -759,7 +767,6 @@
processed_alerts: nil,
product_status: nil,
reference: nil,
- selfie_success: nil,
transaction_reason_code: nil,
transaction_status: nil,
vendor: nil,
@@ -782,6 +789,7 @@
flow_path: 'standard',
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
classification_info: hash_including(
Front: hash_including(ClassName: 'Identification Card', CountryCode: 'USA'),
@@ -827,6 +835,7 @@
flow_path: 'standard',
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
)
@@ -853,6 +862,7 @@
vendor_request_time_in_ms: a_kind_of(Float),
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
doc_type_supported: boolean,
doc_auth_success: boolean,
selfie_status: :not_processed,
@@ -869,7 +879,6 @@
processed_alerts: nil,
product_status: nil,
reference: nil,
- selfie_success: nil,
transaction_reason_code: nil,
transaction_status: nil,
vendor: nil,
@@ -892,6 +901,7 @@
flow_path: 'standard',
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
classification_info: hash_including(:Front, :Back),
)
@@ -934,6 +944,7 @@
flow_path: 'standard',
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
)
@@ -960,6 +971,7 @@
vendor_request_time_in_ms: a_kind_of(Float),
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
doc_type_supported: boolean,
doc_auth_success: boolean,
selfie_status: :not_processed,
@@ -976,7 +988,6 @@
processed_alerts: nil,
product_status: nil,
reference: nil,
- selfie_success: nil,
transaction_reason_code: nil,
transaction_status: nil,
vendor: nil,
@@ -999,6 +1010,7 @@
flow_path: 'standard',
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
classification_info: hash_including(:Front, :Back),
)
@@ -1064,6 +1076,7 @@
flow_path: 'standard',
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
)
@@ -1092,6 +1105,7 @@
vendor_request_time_in_ms: a_kind_of(Float),
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
doc_type_supported: boolean,
doc_auth_success: boolean,
selfie_status: :not_processed,
@@ -1108,7 +1122,6 @@
processed_alerts: nil,
product_status: nil,
reference: nil,
- selfie_success: nil,
transaction_reason_code: nil,
transaction_status: nil,
vendor: nil,
@@ -1152,6 +1165,7 @@
flow_path: 'standard',
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
)
@@ -1182,6 +1196,7 @@
vendor_request_time_in_ms: a_kind_of(Float),
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
doc_type_supported: boolean,
doc_auth_success: boolean,
selfie_status: :not_processed,
@@ -1198,7 +1213,6 @@
processed_alerts: nil,
product_status: nil,
reference: nil,
- selfie_success: nil,
transaction_reason_code: nil,
transaction_status: nil,
vendor: nil,
diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb
index b68ae4480a6..87209d678c5 100644
--- a/spec/features/idv/analytics_spec.rb
+++ b/spec/features/idv/analytics_spec.rb
@@ -69,10 +69,10 @@
width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil
},
'IdV: doc auth image upload form submitted' => {
- success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean
+ success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: nil, liveness_checking_required: boolean
},
'IdV: doc auth image upload vendor pii validation' => {
- success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean, classification_info: {}
+ success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: nil, liveness_checking_required: boolean, classification_info: {}
},
'IdV: doc auth document_capture submitted' => {
success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false
@@ -177,10 +177,10 @@
width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'hybrid', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil
},
'IdV: doc auth image upload form submitted' => {
- success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'hybrid', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean
+ success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'hybrid', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: nil, liveness_checking_required: boolean
},
'IdV: doc auth image upload vendor pii validation' => {
- success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'hybrid', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean, classification_info: {}
+ success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'hybrid', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: nil, liveness_checking_required: boolean, classification_info: {}
},
'IdV: doc auth document_capture submitted' => {
success: true, errors: {}, flow_path: 'hybrid', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false
@@ -282,10 +282,10 @@
width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil
},
'IdV: doc auth image upload form submitted' => {
- success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean
+ success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: nil, liveness_checking_required: boolean
},
'IdV: doc auth image upload vendor pii validation' => {
- success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean, classification_info: {}
+ success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: nil, liveness_checking_required: boolean, classification_info: {}
},
'IdV: doc auth document_capture submitted' => {
success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false
@@ -369,7 +369,7 @@
width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil
},
'IdV: doc auth image upload form submitted' => {
- success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean
+ success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: nil, liveness_checking_required: boolean
},
'IdV: doc auth image upload vendor submitted' => hash_including(success: true, flow_path: 'standard', attention_with_barcode: true, doc_auth_result: 'Attention'),
'IdV: verify in person troubleshooting option clicked' => {
@@ -500,10 +500,10 @@
width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil
},
'IdV: doc auth image upload form submitted' => {
- success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean
+ success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean
},
'IdV: doc auth image upload vendor pii validation' => {
- success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean, classification_info: {}
+ success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), selfie_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean, classification_info: {}
},
'IdV: doc auth document_capture submitted' => {
success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, skip_hybrid_handoff: nil, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false
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 fae43a730d8..825b8a0c0c0 100644
--- a/spec/features/idv/doc_auth/redo_document_capture_spec.rb
+++ b/spec/features/idv/doc_auth/redo_document_capture_spec.rb
@@ -153,6 +153,37 @@
end
end
+ shared_examples_for 'selfie image re-upload not allowed' do
+ it 'stops user submitting the same images again' do
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth document_capture visited',
+ hash_including(redo_document_capture: nil),
+ )
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth image upload form submitted',
+ hash_including(remaining_attempts: 3, attempts: 1),
+ )
+ DocAuth::Mock::DocAuthMockClient.reset!
+ expect(page).not_to have_css(
+ '.usa-error-message[role="alert"]',
+ text: t('doc_auth.errors.doc.resubmit_failed_image'),
+ )
+ attach_selfie
+ expect(page).to have_css(
+ '.usa-error-message[role="alert"]',
+ text: t('doc_auth.errors.doc.resubmit_failed_image'),
+ count: 1,
+ )
+
+ attach_images
+ expect(page).to have_css(
+ '.usa-error-message[role="alert"]',
+ text: t('doc_auth.errors.doc.resubmit_failed_image'),
+ count: 3,
+ )
+ end
+ end
+
shared_examples_for 'inline error for 4xx status shown' do |status|
it "shows inline error for status #{status}" do
error = case status
@@ -251,6 +282,7 @@
and_return(true)
allow_any_instance_of(FederatedProtocols::Oidc).
to receive(:biometric_comparison_required?).and_return(true)
+ allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:fail)
start_idv_from_sp
sign_in_and_2fa_user
complete_doc_auth_steps_before_document_capture_step
@@ -261,10 +293,181 @@
click_try_again
sleep(10)
end
- it_behaves_like 'image re-upload not allowed'
+
+ it_behaves_like 'selfie image re-upload not allowed'
+
it 'shows current existing header' do
expect_doc_capture_page_header(t('doc_auth.headings.review_issues'))
end
end
+
+ context 'when doc auth is success and portait match fails', allow_browser_log: true do
+ before do
+ expect(FeatureManagement).to receive(:idv_allow_selfie_check?).at_least(:once).
+ and_return(true)
+ allow_any_instance_of(FederatedProtocols::Oidc).
+ to receive(:biometric_comparison_required?).and_return(true)
+
+ start_idv_from_sp
+ sign_in_and_2fa_user
+ complete_doc_auth_steps_before_document_capture_step
+ mock_doc_auth_success_face_match_fail
+ attach_images
+ attach_selfie
+ submit_images
+ click_try_again
+ sleep(10)
+ end
+
+ it 'stops user submitting the same images again' do
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth document_capture visited',
+ hash_including(redo_document_capture: nil),
+ )
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth image upload form submitted',
+ hash_including(remaining_attempts: 3, attempts: 1),
+ )
+ DocAuth::Mock::DocAuthMockClient.reset!
+ expect(page).not_to have_css(
+ '.usa-error-message[role="alert"]',
+ text: t('doc_auth.errors.doc.resubmit_failed_image'),
+ )
+
+ attach_selfie
+ expect(page).to have_css(
+ '.usa-error-message[role="alert"]',
+ text: t('doc_auth.errors.doc.resubmit_failed_image'),
+ count: 1,
+ )
+
+ attach_images
+ expect(page).to have_css(
+ '.usa-error-message[role="alert"]',
+ text: t('doc_auth.errors.doc.resubmit_failed_image'),
+ count: 1,
+ )
+ end
+ end
+
+ context 'when doc auth fails and portrait match pass', allow_browser_log: true do
+ before do
+ expect(FeatureManagement).to receive(:idv_allow_selfie_check?).at_least(:once).
+ and_return(true)
+ allow_any_instance_of(FederatedProtocols::Oidc).
+ to receive(:biometric_comparison_required?).and_return(true)
+
+ start_idv_from_sp
+ sign_in_and_2fa_user
+ complete_doc_auth_steps_before_document_capture_step
+ mock_doc_auth_failure_face_match_pass
+ attach_images
+ attach_selfie
+ submit_images
+ click_try_again
+ sleep(10)
+ end
+
+ it 'stops user submitting the same images again' do
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth document_capture visited',
+ hash_including(redo_document_capture: nil),
+ )
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth image upload form submitted',
+ hash_including(remaining_attempts: 3, attempts: 1),
+ )
+ DocAuth::Mock::DocAuthMockClient.reset!
+ expect(page).not_to have_css(
+ '.usa-error-message[role="alert"]',
+ text: t('doc_auth.errors.doc.resubmit_failed_image'),
+ )
+
+ attach_selfie
+ expect(page).not_to have_css(
+ '.usa-error-message[role="alert"]',
+ text: t('doc_auth.errors.doc.resubmit_failed_image'),
+ )
+
+ attach_images
+ expect(page).to have_css(
+ '.usa-error-message[role="alert"]',
+ text: t('doc_auth.errors.doc.resubmit_failed_image'),
+ count: 2,
+ )
+ end
+ end
+
+ context 'when doc auth and portrait match fail', allow_browser_log: true do
+ before do
+ expect(FeatureManagement).to receive(:idv_allow_selfie_check?).at_least(:once).
+ and_return(true)
+ allow_any_instance_of(FederatedProtocols::Oidc).
+ to receive(:biometric_comparison_required?).and_return(true)
+ allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:fail)
+ start_idv_from_sp
+ sign_in_and_2fa_user
+ complete_doc_auth_steps_before_document_capture_step
+ mock_doc_auth_acuant_error_unknown
+ attach_images
+ attach_selfie
+ submit_images
+ click_try_again
+ sleep(10)
+ end
+
+ it_behaves_like 'selfie image re-upload not allowed'
+ end
+
+ context 'when pii validation fails', allow_browser_log: true do
+ before do
+ expect(FeatureManagement).to receive(:idv_allow_selfie_check?).at_least(:once).
+ and_return(true)
+ allow_any_instance_of(FederatedProtocols::Oidc).
+ to receive(:biometric_comparison_required?).and_return(true)
+ pii = Idp::Constants::MOCK_IDV_APPLICANT.dup
+ pii.delete(:address1)
+ allow_any_instance_of(DocAuth::LexisNexis::Responses::TrueIdResponse).
+ to receive(:pii_from_doc).and_return(pii)
+ start_idv_from_sp
+ sign_in_and_2fa_user
+ complete_doc_auth_steps_before_document_capture_step
+ mock_doc_auth_pass_face_match_pass_no_address1
+ attach_images
+ attach_selfie
+ submit_images
+ click_try_again
+ sleep(10)
+ end
+
+ it 'stops user submitting the same images again' do
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth document_capture visited',
+ hash_including(redo_document_capture: nil),
+ )
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth image upload form submitted',
+ hash_including(remaining_attempts: 3, attempts: 1),
+ )
+ DocAuth::Mock::DocAuthMockClient.reset!
+ expect(page).not_to have_css(
+ '.usa-error-message[role="alert"]',
+ text: t('doc_auth.errors.doc.resubmit_failed_image'),
+ )
+
+ attach_selfie
+ expect(page).not_to have_css(
+ '.usa-error-message[role="alert"]',
+ text: t('doc_auth.errors.doc.resubmit_failed_image'),
+ )
+
+ attach_images
+ expect(page).to have_css(
+ '.usa-error-message[role="alert"]',
+ text: t('doc_auth.errors.doc.resubmit_failed_image'),
+ count: 2,
+ )
+ end
+ end
end
end
diff --git a/spec/fixtures/ial2_test_portrait_match_failure.yml b/spec/fixtures/ial2_test_portrait_match_failure.yml
new file mode 100644
index 00000000000..be7ba2e55e1
--- /dev/null
+++ b/spec/fixtures/ial2_test_portrait_match_failure.yml
@@ -0,0 +1,17 @@
+document:
+ first_name: 'John'
+ last_name: 'Doe'
+ address1: 1800 F Street
+ address2: Apt 3
+ city: Bayside
+ state: NY
+ zipcode: '11364'
+ dob: 10/06/1938
+ phone: +1 314-555-1212
+ state_id_jurisdiction: 'ND'
+ state_id_number: '1111111111111'
+doc_auth_result: Passed
+failed_alert: []
+portrait_match_results:
+ FaceMatchResult: Fail
+ FaceErrorMessage: 'Liveness: PoorQuality'
\ No newline at end of file
diff --git a/spec/fixtures/ial2_test_portrait_match_success.yml b/spec/fixtures/ial2_test_portrait_match_success.yml
new file mode 100644
index 00000000000..70b2ec5af5d
--- /dev/null
+++ b/spec/fixtures/ial2_test_portrait_match_success.yml
@@ -0,0 +1,16 @@
+document:
+ first_name: 'John'
+ last_name: 'Doe'
+ address1: 1800 F Street
+ address2: Apt 3
+ city: Bayside
+ state: NY
+ zipcode: '11364'
+ dob: 10/06/1938
+ phone: +1 314-555-1212
+ state_id_jurisdiction: 'ND'
+ state_id_number: '1111111111111'
+doc_auth_result: Passed
+failed_alert: []
+portrait_match_results:
+ FaceMatchResult: Pass
\ No newline at end of file
diff --git a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_with_face_match_pass.json b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_with_face_match_pass.json
new file mode 100644
index 00000000000..d82cd53bb5a
--- /dev/null
+++ b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_with_face_match_pass.json
@@ -0,0 +1,740 @@
+{
+ "Status": {
+ "ConversationId": "31000406185568",
+ "RequestId": "708870588",
+ "TransactionStatus": "failed",
+ "TransactionReasonCode": {
+ "Code": "failed_true_id",
+ "Description": "Failed: TrueID document authentication"
+ },
+ "Reference": "Reference1",
+ "ServerInfo": "bctlsidmapp01.risk.regn.net"
+ },
+ "Products": [
+ {
+ "ProductType": "TrueID",
+ "ExecutedStepName": "True_ID_Step",
+ "ProductConfigurationName": "AndreV3_TrueID_Flow",
+ "ProductStatus": "pass",
+ "ParameterDetails": [
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DocumentName",
+ "Values": [{"Value": "Connecticut (CT) Driver License"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DocAuthResult",
+ "Values": [{"Value": "Failed"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DocIssuerCode",
+ "Values": [{"Value": "CT"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DocIssuerName",
+ "Values": [{"Value": "Connecticut"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DocIssuerType",
+ "Values": [{"Value": "StateProvince"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DocClassCode",
+ "Values": [{"Value": "DriversLicense"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DocClass",
+ "Values": [{"Value": "DriversLicense"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DocClassName",
+ "Values": [{"Value": "Drivers License"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DocIsGeneric",
+ "Values": [{"Value": "false"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DocIssue",
+ "Values": [{"Value": "2009"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DocIssueType",
+ "Values": [{"Value": "Driver License"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DocSize",
+ "Values": [{"Value": "ID1"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "ClassificationMode",
+ "Values": [{"Value": "Automatic"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "OrientationChanged",
+ "Values": [{"Value": "false"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "PresentationChanged",
+ "Values": [{"Value": "false"}]
+ },
+ {
+ "Group": "IMAGE_METRICS_RESULT",
+ "Name": "Side",
+ "Values":[
+ {"Value": "Front"},
+ {"Value": "Back"}
+ ]
+ },
+ {
+ "Group": "IMAGE_METRICS_RESULT",
+ "Name": "GlareMetric",
+ "Values": [
+ {"Value": "100"},
+ {"Value": "100"}
+ ]
+ },
+ {
+ "Group": "IMAGE_METRICS_RESULT",
+ "Name": "SharpnessMetric",
+ "Values":[
+ {"Value": "50"},
+ {"Value": "56"}
+ ]
+ },
+ {
+ "Group": "IMAGE_METRICS_RESULT",
+ "Name": "IsTampered",
+ "Values":[
+ {"Value": "0"},
+ {"Value": "0"}
+ ]
+ },
+ {
+ "Group": "IMAGE_METRICS_RESULT",
+ "Name": "IsCropped",
+ "Values":[
+ {"Value": "1"},
+ {"Value": "1"}
+ ]
+ },
+ {
+ "Group": "IMAGE_METRICS_RESULT",
+ "Name": "HorizontalResolution",
+ "Values":[
+ {"Value": "418"},
+ {"Value": "420"}
+ ]
+ },
+ {
+ "Group": "IMAGE_METRICS_RESULT",
+ "Name": "VerticalResolution",
+ "Values":[
+ {"Value": "418"},
+ {"Value": "420"}
+ ]
+ },
+ {
+ "Group": "IMAGE_METRICS_RESULT",
+ "Name": "Light",
+ "Values":[
+ {"Value": "White"},
+ {"Value": "White"}
+ ]
+ },
+ {
+ "Group": "IMAGE_METRICS_RESULT",
+ "Name": "MimeType",
+ "Values":[
+ {"Value": "image/vnd.ms-photo"},
+ {"Value": "image/vnd.ms-photo"}
+ ]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "FullName",
+ "Values": [{"Value": "EVAN M MOZINGO"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Sex",
+ "Values": [{"Value": "Male"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Age",
+ "Values": [{"Value": "30"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DOB_Year",
+ "Values": [{"Value": "1989"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DOB_Month",
+ "Values": [{"Value": "9"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "DOB_Day",
+ "Values": [{"Value": "11"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "ExpirationDate_Year",
+ "Values": [{"Value": "2017"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "ExpirationDate_Month",
+ "Values": [{"Value": "9"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "ExpirationDate_Day",
+ "Values": [{"Value": "11"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Portrait",
+ "Values": [{"Value": "/9j/4AAQSkZJRgABAQEBogGiAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9\nPDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhC\nY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAAR\nCAI1AagDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA\nAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK\nFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG\nh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl\n5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA\nAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk\nNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE\nhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk\n5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDvOlVr2QRwjOeeOKtGqmok/ZwB6jmluNu2\npkzbmbcOcimEFgQykVKRgDpTQSDkkjHr0o9mP61IhiTy3KgfK3OaWVdwAGfYipdpUeYr8Z5B\nGaeyjdnGaXsx/WplSOHzThl2qvRQetOaz3jKnB9Ksxrt471E1yc7UGOTnn0o9mH1qaKcqOow\nycDrg9aiVv3i7Uz61baRm4AzmkkiUehOKr2RP1yYTNyAFIyASRWeYpPNOVOCeuOlX1kUrhk6\ncD3pnGSCPeq9iH12aK+XjJwhZfam+ZLliEOPp1q2MFscZ96axIJCn5fY8U/ZIX16ZBlin3Tg\ne3WkzuOAMAdR71PkClwWPzfnR7JC+vVCDgMPl57UjrzvCAMeBz0qdsAfSmgADFHsg+vVCBQT\n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\nRxR5agcDGKKKLABRc520UUUAf//Z"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_1_AlertName",
+ "Values": [{"Value": "Visible Pattern"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_2_AlertName",
+ "Values": [{"Value": "Visible Pattern"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_3_AlertName",
+ "Values": [{"Value": "Visible Pattern"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_4_AlertName",
+ "Values": [{"Value": "Visible Pattern"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_5_AlertName",
+ "Values": [{"Value": "1D Control Number Valid"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_6_AlertName",
+ "Values": [{"Value": "2D Barcode Content"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_7_AlertName",
+ "Values": [{"Value": "Control Number Crosscheck"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_8_AlertName",
+ "Values": [{"Value": "Document Expired"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_9_AlertName",
+ "Values": [{"Value": "2D Barcode Read"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_10_AlertName",
+ "Values": [{"Value": "Birth Date Crosscheck"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_11_AlertName",
+ "Values": [{"Value": "Birth Date Valid"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_12_AlertName",
+ "Values": [{"Value": "Document Classification"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_13_AlertName",
+ "Values": [{"Value": "Document Crosscheck Aggregation"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_14_AlertName",
+ "Values": [{"Value": "Document Number Crosscheck"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_15_AlertName",
+ "Values": [{"Value": "Expiration Date Crosscheck"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_16_AlertName",
+ "Values": [{"Value": "Expiration Date Valid"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_17_AlertName",
+ "Values": [{"Value": "Full Name Crosscheck"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_18_AlertName",
+ "Values": [{"Value": "Issue Date Crosscheck"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_19_AlertName",
+ "Values": [{"Value": "Issue Date Valid"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_20_AlertName",
+ "Values": [{"Value": "Layout Valid"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_21_AlertName",
+ "Values": [{"Value": "Sex Crosscheck"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_22_AlertName",
+ "Values": [{"Value": "Visible Color Response"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_23_AlertName",
+ "Values": [{"Value": "Visible Pattern"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_24_AlertName",
+ "Values": [{"Value": "Visible Photo Characteristics"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_1_AuthenticationResult",
+ "Values": [{
+ "Value": "Failed",
+ "Detail": "Verified the presence of a pattern on the visible image."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_2_AuthenticationResult",
+ "Values": [{
+ "Value": "Failed",
+ "Detail": "Verified the presence of a pattern on the visible image."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_3_AuthenticationResult",
+ "Values": [{
+ "Value": "Failed",
+ "Detail": "Verified the presence of a pattern on the visible image."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_4_AuthenticationResult",
+ "Values": [{
+ "Value": "Failed",
+ "Detail": "Verified the presence of a pattern on the visible image."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_5_AuthenticationResult",
+ "Values": [{
+ "Value": "Failed",
+ "Detail": "Checks whether the 1D barcode is indicative of known-fake documents."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_6_AuthenticationResult",
+ "Values": [{
+ "Value": "Failed",
+ "Detail": "Checked the contents of the two-dimensional barcode on the document."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_7_AuthenticationResult",
+ "Values": [{
+ "Value": "Caution",
+ "Detail": "Compare the machine-readable control number field to the human-readable control number field."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_8_AuthenticationResult",
+ "Values": [{
+ "Value": "Attention",
+ "Detail": "Checked if the document is expired."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_9_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Verified that the two-dimensional barcode on the document was read successfully."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_10_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Compare the machine-readable birth date field to the human-readable birth date field."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_11_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Verified that the birth date is valid."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_12_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Verified that the type of document is supported and is able to be fully authenticated."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_13_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Compared the machine-readable fields to the human-readable fields."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_14_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Compare the machine-readable document number field to the human-readable document number field."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_15_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Compare the machine-readable expiration date field to the human-readable expiration date field."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_16_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Verified that the expiration date is valid."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_17_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Compare the machine-readable full name field to the human-readable full name field."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_18_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Compare the machine-readable issue date field to the human-readable issue date field."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_19_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Verified that the issue date is valid."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_20_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Verified that the layout of the document is correct."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_21_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Compare the machine-readable sex field to the human-readable sex field."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_22_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Verified the color response of an element on the visible image."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_23_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Verified the presence of a pattern on the visible image."
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_24_AuthenticationResult",
+ "Values": [{
+ "Value": "Passed",
+ "Detail": "Verifies the visible characteristics of the Photo"
+ }]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_1_Regions",
+ "Values": [{"Value": "Stylized DOB Label"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_2_Regions",
+ "Values": [{"Value": "Expires Label"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_3_Regions",
+ "Values": [{"Value": "Inches"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_4_Regions",
+ "Values": [{"Value": "Background Center"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_20_Regions",
+ "Values":[
+ {"Value": "2D Barcode Bottom Left"},
+ {"Value": "2D Barcode Top Left"}
+ ]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_22_Regions",
+ "Values": [{"Value": "Background Salmon"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_23_Regions",
+ "Values": [{"Value": "Background Boat"}]
+ },
+ {
+ "Group": "AUTHENTICATION_RESULT",
+ "Name": "Alert_24_Regions",
+ "Values": [{"Value": "Photo"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_FullName",
+ "Values": [{"Value": "EVAN M MOZINGO"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_Surname",
+ "Values": [{"Value": "MOZINGO"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_GivenName",
+ "Values": [{"Value": "EVAN M"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_FirstName",
+ "Values": [{"Value": "EVAN"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_MiddleName",
+ "Values": [{"Value": "M"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_DOB_Year",
+ "Values": [{"Value": "1989"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_DOB_Month",
+ "Values": [{"Value": "9"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_DOB_Day",
+ "Values": [{"Value": "11"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_DocumentClassName",
+ "Values": [{"Value": "Drivers License"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_DocumentNumber",
+ "Values": [{"Value": "096760377"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_ExpirationDate_Year",
+ "Values": [{"Value": "2017"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_ExpirationDate_Month",
+ "Values": [{"Value": "9"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_xpirationDate_Day",
+ "Values": [{"Value": "11"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_IssuingStateCode",
+ "Values": [{"Value": "CT"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_IssuingStateName",
+ "Values": [{"Value": "Connecticut"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_Address",
+ "Values": [{"Value": "1 MUDRY FARM RD
BRISTOL, CT 02809-2366"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_AddressLine1",
+ "Values": [{"Value": "1 MUDRY FARM RD"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_City",
+ "Values": [{"Value": "BRISTOL"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_State",
+ "Values": [{"Value": "CT"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_PostalCode",
+ "Values": [{"Value": "02809-2366"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_Sex",
+ "Values": [{"Value": "M"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_ControlNumber",
+ "Values": [{"Value": "demo a11230009"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_Height",
+ "Values": [{"Value": "5' 9\""}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_IssueDate_Year",
+ "Values": [{"Value": "2010"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_IssueDate_Month",
+ "Values": [{"Value": "10"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_IssueDate_Day",
+ "Values": [{"Value": "12"}]
+ },
+ {
+ "Group": "IDAUTH_FIELD_DATA",
+ "Name": "Fields_LicenseClass",
+ "Values": [{"Value": "D"}]
+ },
+ {
+ "Group": "PORTRAIT_MATCH_RESULT",
+ "Name": "FaceMatchResult",
+ "Values": [{"Value": "Pass"}]
+ },
+ {
+ "Group": "PORTRAIT_MATCH_RESULT",
+ "Name": "FaceMatchScore",
+ "Values": [{"Value": "96"}]
+ },
+ {
+ "Group": "PORTRAIT_MATCH_RESULT",
+ "Name": "FaceStatusCode",
+ "Values": [{"Value": "1"}]
+ },
+ {
+ "Group": "PORTRAIT_MATCH_RESULT",
+ "Name": "FaceErrorMessage",
+ "Values": [{"Value": "Successful. Liveness: Live"}]
+ }
+ ]
+ },
+ {
+ "ProductType": "TrueID_Decision",
+ "ExecutedStepName": "Decision",
+ "ProductConfigurationName": "TRUEID_FAIL",
+ "ProductStatus": "fail",
+ "ProductReason": {
+ "Code": "failed_true_id",
+ "Description": "Failed: TrueID document authentication"
+ }
+ }
+ ]
+}
diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb
index ad79ed7cf8b..9e936aab656 100644
--- a/spec/forms/idv/api_image_upload_form_spec.rb
+++ b/spec/forms/idv/api_image_upload_form_spec.rb
@@ -124,6 +124,7 @@
flow_path: anything,
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
)
@@ -164,6 +165,7 @@
vendor_request_time_in_ms: a_kind_of(Float),
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
doc_type_supported: boolean,
liveness_checking_required: boolean,
log_alert_results: nil,
@@ -173,7 +175,6 @@
reference: nil,
selfie_live: boolean,
selfie_quality_good: boolean,
- selfie_success: nil,
doc_auth_success: boolean,
selfie_status: anything,
vendor: nil,
@@ -187,10 +188,119 @@
expect(response).to be_a_kind_of DocAuth::Response
expect(response.success?).to eq(true)
+ expect(response.doc_auth_success?).to eq(true)
+ expect(response.selfie_status).to eq(:not_processed)
expect(response.errors).to eq({})
expect(response.attention_with_barcode?).to eq(false)
expect(response.pii_from_doc).to eq(Idp::Constants::MOCK_IDV_APPLICANT)
end
+
+ context 'when liveness check is required' do
+ let(:liveness_checking_required) { true }
+ let(:back_image) { DocAuthImageFixtures.portrait_match_success_yaml }
+ let(:selfie_image) { DocAuthImageFixtures.selfie_image_multipart }
+ it 'logs analytics' do
+ expect(irs_attempts_api_tracker).to receive(:idv_document_upload_submitted).with(
+ {
+ address: '1800 F Street',
+ date_of_birth: '1938-10-06',
+ document_back_image_filename: nil,
+ document_expiration: nil,
+ document_front_image_filename: nil,
+ document_image_encryption_key: nil,
+ document_issued: nil,
+ document_number: '1111111111111',
+ document_state: 'NY',
+ first_name: 'John',
+ last_name: 'Doe',
+ success: true,
+ },
+ )
+
+ form.submit
+
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth image upload form submitted',
+ success: true,
+ errors: {},
+ attempts: 1,
+ remaining_attempts: 3,
+ user_id: document_capture_session.user.uuid,
+ flow_path: anything,
+ front_image_fingerprint: an_instance_of(String),
+ back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: an_instance_of(String),
+ liveness_checking_required: boolean,
+ )
+
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth image upload vendor submitted',
+ address_line2_present: nil,
+ alert_failure_count: nil,
+ async: false,
+ attempts: 1,
+ attention_with_barcode: false,
+ billed: true,
+ client_image_metrics: {
+ back: {
+ height: 20,
+ mimeType: 'image/png',
+ source: 'upload',
+ width: 20,
+ },
+ front: {
+ height: 40,
+ mimeType: 'image/png',
+ source: 'upload',
+ width: 40,
+ },
+ },
+ conversation_id: nil,
+ decision_product_status: nil,
+ doc_auth_result: 'Passed',
+ errors: {},
+ exception: nil,
+ flow_path: anything,
+ image_metrics: nil,
+ liveness_checking_required: boolean,
+ log_alert_results: nil,
+ portrait_match_results: anything,
+ processed_alerts: nil,
+ product_status: nil,
+ reference: nil,
+ remaining_attempts: 3,
+ state: 'NY',
+ state_id_type: nil,
+ success: true,
+ 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),
+ selfie_image_fingerprint: an_instance_of(String),
+ doc_type_supported: boolean,
+ selfie_live: boolean,
+ selfie_quality_good: boolean,
+ doc_auth_success: boolean,
+ selfie_status: :success,
+ vendor: nil,
+ transaction_status: nil,
+ transaction_reason_code: nil,
+ )
+ end
+
+ it 'returns the expected response' do
+ response = form.submit
+
+ expect(response).to be_a_kind_of DocAuth::Response
+ expect(response.success?).to eq(true)
+ expect(response.doc_auth_success?).to eq(true)
+ expect(response.selfie_check_performed?).to eq(true)
+ expect(response.selfie_status).to eq(:success)
+ expect(response.errors).to eq({})
+ expect(response.attention_with_barcode?).to eq(false)
+ # expect(response.pii_from_doc).to eq(Idp::Constants::MOCK_IDV_APPLICANT)
+ end
+ end
end
context 'image data returns unknown errors' do
@@ -240,6 +350,7 @@
flow_path: anything,
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
)
end
@@ -275,10 +386,14 @@
end
context 'posting images to client fails' do
+ let(:errors) do
+ { front: 'glare' }
+ end
+
let(:failed_response) do
DocAuth::Response.new(
success: false,
- errors: { front: 'glare' },
+ errors: errors,
extra: { remaining_attempts: IdentityConfig.store.doc_auth_max_attempts - 1 },
)
end
@@ -293,6 +408,8 @@
expect(response).to be_a_kind_of DocAuth::Response
expect(response.success?).to eq(false)
+ expect(response.doc_auth_success?).to eq(false)
+ expect(response.selfie_status).to eq(:not_processed)
expect(response.attention_with_barcode?).to eq(false)
expect(response.pii_from_doc).to eq({})
expect(response.doc_auth_success?).to eq(false)
@@ -335,6 +452,40 @@
),
)
end
+
+ context 'selfie is checked for liveness' do
+ let(:liveness_checking_required) { true }
+ let(:selfie_image) { DocAuthImageFixtures.selfie_image_multipart }
+ let(:errors) do
+ { selfie: 'glare' }
+ end
+ let(:back_image) { DocAuthImageFixtures.portrait_match_fail_yaml }
+
+ before do
+ allow(failed_response).to receive(:doc_auth_success?).and_return(true)
+ allow(failed_response).to receive(:selfie_status).and_return(:fail)
+ end
+
+ it 'includes client response errors' do
+ response = form.submit
+ expect(response.errors[:front]).to be_nil
+ expect(response.errors[:back]).to be_nil
+ expect(response.errors[:selfie]).to eq('glare')
+ end
+
+ it 'keeps fingerprints of failed image and triggers error when submit same image' do
+ form.submit
+ session = DocumentCaptureSession.find_by(uuid: document_capture_session_uuid)
+ capture_result = session.load_result
+ expect(capture_result.failed_front_image_fingerprints).to match_array([])
+ expect(capture_result.failed_back_image_fingerprints).to match_array([])
+ expect(capture_result.failed_selfie_image_fingerprints.length).to eq(1)
+ response = form.submit
+ expect(response.errors).to have_key(:selfie)
+ expect(response.errors).
+ to have_value([I18n.t('doc_auth.errors.doc.resubmit_failed_image')])
+ end
+ end
end
context 'PII validation from client response fails' do
@@ -382,7 +533,8 @@
form.submit
session = DocumentCaptureSession.find_by(uuid: document_capture_session_uuid)
capture_result = session.load_result
- expect(capture_result.failed_front_image_fingerprints).not_to match_array([])
+ expect(capture_result.failed_front_image_fingerprints.length).to eq(1)
+ expect(capture_result.failed_back_image_fingerprints.length).to eq(1)
response = form.submit
expect(response.errors).to have_key(:front)
expect(response.errors).to have_value([I18n.t('doc_auth.errors.doc.resubmit_failed_image')])
@@ -394,10 +546,41 @@
flow_path: anything,
front_image_fingerprint: an_instance_of(String),
back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
liveness_checking_required: boolean,
side: 'both',
)
end
+
+ context 'when selfie is checked for liveness' do
+ let(:selfie_image) { DocAuthImageFixtures.selfie_image_multipart }
+ let(:back_image) { DocAuthImageFixtures.portrait_match_success_yaml }
+ it 'keeps fingerprints of failed image and triggers error when submit same image' do
+ form.submit
+ session = DocumentCaptureSession.find_by(uuid: document_capture_session_uuid)
+ capture_result = session.load_result
+ expect(capture_result.failed_front_image_fingerprints.length).to eq(1)
+ expect(capture_result.failed_back_image_fingerprints.length).to eq(1)
+ expect(capture_result.failed_selfie_image_fingerprints).to be_nil
+ response = form.submit
+ expect(response.errors).to have_key(:front)
+ expect(response.errors).to have_key(:back)
+ expect(response.errors).
+ to have_value([I18n.t('doc_auth.errors.doc.resubmit_failed_image')])
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: failed doc image resubmitted',
+ attempts: 1,
+ remaining_attempts: 3,
+ user_id: document_capture_session.user.uuid,
+ flow_path: anything,
+ front_image_fingerprint: an_instance_of(String),
+ back_image_fingerprint: an_instance_of(String),
+ selfie_image_fingerprint: nil,
+ liveness_checking_required: boolean,
+ side: 'both',
+ )
+ end
+ end
end
describe 'encrypted document storage' do
@@ -526,6 +709,7 @@
allow(client_response).to receive(:network_error?).and_return(false)
allow(client_response).to receive(:errors).and_return(errors)
allow(client_response).to receive(:doc_auth_success?).and_return(false)
+ allow(client_response).to receive(:selfie_status).and_return(:not_processed)
form.send(:validate_form)
capture_result = form.send(:store_failed_images, client_response, doc_pii_response)
expect(capture_result[:front]).not_to be_empty
@@ -539,6 +723,7 @@
allow(client_response).to receive(:network_error?).and_return(false)
allow(client_response).to receive(:errors).and_return(errors)
allow(client_response).to receive(:doc_auth_success?).and_return(false)
+ allow(client_response).to receive(:selfie_status).and_return(:not_processed)
form.send(:validate_form)
capture_result = form.send(:store_failed_images, client_response, doc_pii_response)
expect(capture_result[:front]).not_to be_empty
@@ -552,6 +737,7 @@
allow(client_response).to receive(:network_error?).and_return(false)
allow(client_response).to receive(:errors).and_return(errors)
allow(client_response).to receive(:doc_auth_success?).and_return(false)
+ allow(client_response).to receive(:selfie_status).and_return(:not_processed)
form.send(:validate_form)
capture_result = form.send(:store_failed_images, client_response, doc_pii_response)
expect(capture_result[:front]).not_to be_empty
@@ -581,6 +767,7 @@
allow(client_response).to receive(:errors).and_return(errors)
allow(client_response).to receive(:doc_auth_success?).and_return(false)
allow(doc_pii_response).to receive(:success?).and_return(false)
+ allow(client_response).to receive(:selfie_status).and_return(:not_processed)
form.send(:validate_form)
capture_result = form.send(:store_failed_images, client_response, doc_pii_response)
expect(capture_result[:front]).not_to be_empty
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 8fae8f15415..6f8fa036d4f 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -96,27 +96,4 @@
end
end
end
-
- describe '#selfie_status_from_response' do
- context 'the response does not have selfie_status method defined on it' do
- let(:success) { true }
- let(:errors) { {} }
- let(:exception) { nil }
- let(:pii_from_doc) { {} }
- let(:attention_with_barcode) { false }
- let(:response) do
- DocAuth::Response.new(
- success: success,
- errors: errors,
- exception: exception,
- pii_from_doc: pii_from_doc,
- attention_with_barcode: attention_with_barcode,
- )
- end
-
- it 'returns :not_processed' do
- expect(selfie_status_from_response(response)).to eq(:not_processed)
- end
- end
- end
end
diff --git a/spec/models/document_capture_session_spec.rb b/spec/models/document_capture_session_spec.rb
index 406a02a7929..4deee580d0a 100644
--- a/spec/models/document_capture_session_spec.rb
+++ b/spec/models/document_capture_session_spec.rb
@@ -95,6 +95,7 @@
record.store_failed_auth_data(
front_image_fingerprint: 'fingerprint1',
back_image_fingerprint: nil,
+ selfie_image_fingerprint: nil,
doc_auth_success: false,
selfie_status: :not_processed,
)
@@ -110,12 +111,13 @@
expect(result.selfie_status).to eq(:not_processed)
end
- it 'saves failed image finterprints' do
+ it 'saves failed image fingerprints' do
record = DocumentCaptureSession.new(result_id: SecureRandom.uuid)
record.store_failed_auth_data(
front_image_fingerprint: 'fingerprint1',
back_image_fingerprint: nil,
+ selfie_image_fingerprint: nil,
doc_auth_success: false,
selfie_status: :not_processed,
)
@@ -124,6 +126,7 @@
record.store_failed_auth_data(
front_image_fingerprint: 'fingerprint2',
back_image_fingerprint: 'fingerprint3',
+ selfie_image_fingerprint: nil,
doc_auth_success: false,
selfie_status: :not_processed,
)
@@ -132,14 +135,59 @@
expect(old_result.failed_front_image?('fingerprint1')).to eq(true)
expect(old_result.failed_front_image?('fingerprint2')).to eq(false)
expect(old_result.failed_back_image?('fingerprint3')).to eq(false)
+ expect(old_result.failed_selfie_image_fingerprints).to be_nil
expect(old_result.doc_auth_success).to eq(false)
expect(old_result.selfie_status).to eq(:not_processed)
expect(new_result.failed_front_image?('fingerprint1')).to eq(true)
expect(new_result.failed_front_image?('fingerprint2')).to eq(true)
expect(new_result.failed_back_image?('fingerprint3')).to eq(true)
+ expect(new_result.failed_selfie_image_fingerprints).to be_nil
expect(new_result.doc_auth_success).to eq(false)
expect(new_result.selfie_status).to eq(:not_processed)
+
+ old_result = new_result
+
+ record.store_failed_auth_data(
+ front_image_fingerprint: 'fingerprint2',
+ back_image_fingerprint: 'fingerprint3',
+ selfie_image_fingerprint: 'fingerprint4',
+ doc_auth_success: false,
+ selfie_status: :fail,
+ )
+ new_result = record.load_result
+
+ expect(old_result.failed_front_image?('fingerprint1')).to eq(true)
+ expect(old_result.failed_front_image?('fingerprint2')).to eq(true)
+ expect(old_result.failed_back_image?('fingerprint3')).to eq(true)
+ expect(old_result.failed_selfie_image_fingerprints).to be_nil
+ expect(old_result.doc_auth_success).to eq(false)
+ expect(old_result.selfie_status).to eq(:not_processed)
+
+ expect(new_result.failed_front_image_fingerprints.length).to eq(2)
+ expect(new_result.failed_back_image_fingerprints.length).to eq(1)
+ expect(new_result.failed_selfie_image?('fingerprint4')).to eq(true)
+ expect(new_result.doc_auth_success).to eq(false)
+ expect(new_result.selfie_status).to eq(:fail)
+ end
+
+ context 'when selfie is successful' do
+ it 'does not add selfie to failed image fingerprints' do
+ record = DocumentCaptureSession.new(result_id: SecureRandom.uuid)
+
+ record.store_failed_auth_data(
+ front_image_fingerprint: 'fingerprint1',
+ back_image_fingerprint: 'fingerprint2',
+ selfie_image_fingerprint: 'fingerprint3',
+ doc_auth_success: false,
+ selfie_status: :pass,
+ )
+ result = record.load_result
+
+ expect(result.failed_front_image?('fingerprint1')).to eq(true)
+ expect(result.failed_back_image?('fingerprint2')).to eq(true)
+ expect(result.failed_selfie_image_fingerprints).to be_nil
+ end
end
end
end
diff --git a/spec/presenters/image_upload_response_presenter_spec.rb b/spec/presenters/image_upload_response_presenter_spec.rb
index 03e81f90681..58e36fa5134 100644
--- a/spec/presenters/image_upload_response_presenter_spec.rb
+++ b/spec/presenters/image_upload_response_presenter_spec.rb
@@ -109,7 +109,7 @@
let(:extra_attributes) do
{ remaining_attempts: 0,
flow_path: 'standard',
- failed_image_fingerprints: { back: [], front: ['12345'] } }
+ failed_image_fingerprints: { back: [], front: ['12345'], selfie: [] } }
end
let(:form_response) do
FormResponse.new(
@@ -130,7 +130,7 @@
remaining_attempts: 0,
ocr_pii: nil,
doc_type_supported: true,
- failed_image_fingerprints: { back: [], front: ['12345'] },
+ failed_image_fingerprints: { back: [], front: ['12345'], selfie: [] },
}
expect(presenter.as_json).to eq expected
@@ -140,7 +140,7 @@
let(:extra_attributes) do
{ remaining_attempts: 0,
flow_path: 'hybrid',
- failed_image_fingerprints: { back: [], front: ['12345'] } }
+ failed_image_fingerprints: { back: [], front: ['12345'], selfie: [] } }
end
it 'returns hash of properties redirecting to capture_complete' do
@@ -152,7 +152,7 @@
remaining_attempts: 0,
ocr_pii: nil,
doc_type_supported: true,
- failed_image_fingerprints: { back: [], front: ['12345'] },
+ failed_image_fingerprints: { back: [], front: ['12345'], selfie: [] },
}
expect(presenter.as_json).to eq expected
@@ -181,7 +181,7 @@
remaining_attempts: 3,
ocr_pii: nil,
doc_type_supported: true,
- failed_image_fingerprints: { back: [], front: [] },
+ failed_image_fingerprints: { back: [], front: [], selfie: [] },
}
expect(presenter.as_json).to eq expected
@@ -208,7 +208,7 @@
remaining_attempts: 3,
ocr_pii: nil,
doc_type_supported: true,
- failed_image_fingerprints: { front: [], back: [] },
+ failed_image_fingerprints: { front: [], back: [], selfie: [] },
}
expect(presenter.as_json).to eq expected
@@ -245,7 +245,7 @@
remaining_attempts: 0,
ocr_pii: nil,
doc_type_supported: true,
- failed_image_fingerprints: { front: [], back: [] },
+ failed_image_fingerprints: { front: [], back: [], selfie: [] },
}
expect(presenter.as_json).to eq expected
@@ -262,7 +262,7 @@
remaining_attempts: 0,
ocr_pii: nil,
doc_type_supported: true,
- failed_image_fingerprints: { back: [], front: [] },
+ failed_image_fingerprints: { back: [], front: [], selfie: [] },
}
expect(presenter.as_json).to eq expected
@@ -290,7 +290,7 @@
remaining_attempts: 3,
ocr_pii: Idp::Constants::MOCK_IDV_APPLICANT.slice(:first_name, :last_name, :dob),
doc_type_supported: true,
- failed_image_fingerprints: { back: [], front: [] },
+ failed_image_fingerprints: { back: [], front: [], selfie: [] },
}
expect(presenter.as_json).to eq expected
@@ -316,7 +316,7 @@
remaining_attempts: 3,
ocr_pii: Idp::Constants::MOCK_IDV_APPLICANT.slice(:first_name, :last_name, :dob),
doc_type_supported: true,
- failed_image_fingerprints: { back: [], front: [] },
+ failed_image_fingerprints: { back: [], front: [], selfie: [] },
}
expect(presenter.as_json).to eq expected
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 d71d3005ba8..1ce36b10b1b 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
@@ -137,6 +137,7 @@
selfie_status: :not_processed,
selfie_live: true,
selfie_quality_good: true,
+ liveness_enabled: false,
)
passed_alerts = response_hash.dig(:processed_alerts, :passed)
passed_alerts.each do |alert|
@@ -380,6 +381,7 @@ def get_decision_product(resp)
selfie_status: :fail,
selfie_live: true,
selfie_quality_good: false,
+ liveness_enabled: false,
)
end
it 'produces appropriate errors with document tampering' do
diff --git a/spec/support/doc_auth_image_fixtures.rb b/spec/support/doc_auth_image_fixtures.rb
index 4063e925fe8..188e3f3d4d7 100644
--- a/spec/support/doc_auth_image_fixtures.rb
+++ b/spec/support/doc_auth_image_fixtures.rb
@@ -47,6 +47,22 @@ def self.error_yaml_no_db_multipart
Rack::Test::UploadedFile.new(path, Mime[:yaml])
end
+ def self.portrait_match_success_yaml
+ path = File.join(
+ File.dirname(__FILE__),
+ '../fixtures/ial2_test_portrait_match_success.yml',
+ )
+ Rack::Test::UploadedFile.new(path, Mime[:yaml])
+ end
+
+ def self.portrait_match_fail_yaml
+ path = File.join(
+ File.dirname(__FILE__),
+ '../fixtures/ial2_test_portrait_match_failure.yml',
+ )
+ Rack::Test::UploadedFile.new(path, Mime[:yaml])
+ end
+
def self.fixture_path(filename)
File.join(
File.dirname(__FILE__),
diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb
index a5f8464ceda..4c763b6ab86 100644
--- a/spec/support/features/doc_auth_helper.rb
+++ b/spec/support/features/doc_auth_helper.rb
@@ -252,6 +252,70 @@ def mock_doc_auth_attention_with_barcode
)
end
+ def mock_doc_auth_success_face_match_fail
+ failure_response = instance_double(
+ Faraday::Response,
+ status: 200,
+ body: LexisNexisFixtures.true_id_response_with_face_match_fail,
+ )
+ DocAuth::Mock::DocAuthMockClient.mock_response!(
+ method: :get_results,
+ response: DocAuth::LexisNexis::Responses::TrueIdResponse.new(
+ failure_response,
+ DocAuth::LexisNexis::Config.new,
+ true, # liveness_checking_enabled
+ ),
+ )
+ end
+
+ def mock_doc_auth_failure_face_match_fail
+ failure_response = instance_double(
+ Faraday::Response,
+ status: 200,
+ body: LexisNexisFixtures.true_id_response_failure_no_liveness,
+ )
+ DocAuth::Mock::DocAuthMockClient.mock_response!(
+ method: :get_results,
+ response: DocAuth::LexisNexis::Responses::TrueIdResponse.new(
+ failure_response,
+ DocAuth::LexisNexis::Config.new,
+ true, # liveness_checking_enabled
+ ),
+ )
+ end
+
+ def mock_doc_auth_failure_face_match_pass
+ failure_response = instance_double(
+ Faraday::Response,
+ status: 200,
+ body: LexisNexisFixtures.true_id_response_failure_with_face_match_pass,
+ )
+ DocAuth::Mock::DocAuthMockClient.mock_response!(
+ method: :get_results,
+ response: DocAuth::LexisNexis::Responses::TrueIdResponse.new(
+ failure_response,
+ DocAuth::LexisNexis::Config.new,
+ true, # liveness_checking_enabled
+ ),
+ )
+ end
+
+ def mock_doc_auth_pass_face_match_pass_no_address1
+ response = instance_double(
+ Faraday::Response,
+ status: 200,
+ body: LexisNexisFixtures.true_id_response_success_with_liveness,
+ )
+ DocAuth::Mock::DocAuthMockClient.mock_response!(
+ method: :get_results,
+ response: DocAuth::LexisNexis::Responses::TrueIdResponse.new(
+ response,
+ DocAuth::LexisNexis::Config.new,
+ true, # liveness_checking_enabled
+ ),
+ )
+ end
+
def mock_doc_auth_trueid_http_non2xx_status(status)
network_error_response = instance_double(
Faraday::Response,
diff --git a/spec/support/lexis_nexis_fixtures.rb b/spec/support/lexis_nexis_fixtures.rb
index d1929aed7ce..922cf8026d2 100644
--- a/spec/support/lexis_nexis_fixtures.rb
+++ b/spec/support/lexis_nexis_fixtures.rb
@@ -168,6 +168,10 @@ def true_id_response_with_face_match_fail
read_fixture_file_at_path('true_id/true_id_response_with_face_match_fail.json')
end
+ def true_id_response_failure_with_face_match_pass
+ read_fixture_file_at_path('true_id/true_id_response_failure_with_face_match_pass.json')
+ end
+
def true_id_response_failure_no_liveness
read_fixture_file_at_path('true_id/true_id_response_failure_no_liveness.json')
end
From 39cdd8d4dc00e5e6829d6c8f35a8d4dcbad8fbce Mon Sep 17 00:00:00 2001
From: Charley Ferguson
Date: Wed, 31 Jan 2024 14:21:52 -0500
Subject: [PATCH 02/25] LG-11893: Selfie Liveness Errors on the FE (#9975)
changelog: User-Facing Improvements, In-Person Proofing, Change error messages when selfie upload fails.
---
.../document-capture-review-issues.tsx | 4 +
.../components/document-capture-warning.tsx | 29 +++++--
.../components/document-capture.tsx | 2 +
.../components/review-issues-step.tsx | 4 +
.../components/unknown-error.tsx | 38 ++++++++-
.../document-capture/context/upload.tsx | 10 +++
.../document-capture/services/upload.ts | 9 +++
app/services/doc_auth/errors.rb | 5 +-
app/services/doc_auth/selfie_concern.rb | 10 +--
app/services/doc_auth_router.rb | 7 ++
config/locales/doc_auth/en.yml | 5 ++
config/locales/doc_auth/es.yml | 5 ++
config/locales/doc_auth/fr.yml | 6 ++
config/locales/errors/en.yml | 1 +
config/locales/errors/es.yml | 1 +
config/locales/errors/fr.yml | 1 +
.../idv/doc_auth/document_capture_spec.rb | 78 +++++++++++++++++++
.../ial2_test_credential_no_liveness.yml | 20 +++++
.../ial2_test_credential_poor_quality.yml | 20 +++++
.../mock/doc_auth_mock_client_spec.rb | 8 +-
20 files changed, 244 insertions(+), 19 deletions(-)
create mode 100644 spec/fixtures/ial2_test_credential_no_liveness.yml
create mode 100644 spec/fixtures/ial2_test_credential_poor_quality.yml
diff --git a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx
index 84c74fbcba0..4a08856d737 100644
--- a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx
+++ b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx
@@ -17,6 +17,7 @@ import type { ReviewIssuesStepValue } from './review-issues-step';
interface DocumentCaptureReviewIssuesProps extends FormStepComponentProps {
isFailedDocType: boolean;
+ isFailedSelfieLivenessOrQuality: boolean;
remainingAttempts: number;
captureHints: boolean;
hasDismissed: boolean;
@@ -24,6 +25,7 @@ interface DocumentCaptureReviewIssuesProps extends FormStepComponentProps undefined,
@@ -52,6 +54,8 @@ function DocumentCaptureReviewIssues({
unknownFieldErrors={unknownFieldErrors}
remainingAttempts={remainingAttempts}
isFailedDocType={isFailedDocType}
+ isFailedSelfieLivenessOrQuality={isFailedSelfieLivenessOrQuality}
+ altIsFailedSelfieDontIncludeAttempts
altFailedDocTypeMsg={isFailedDocType ? t('doc_auth.errors.doc.wrong_id_type_html') : null}
hasDismissed={hasDismissed}
/>
diff --git a/app/javascript/packages/document-capture/components/document-capture-warning.tsx b/app/javascript/packages/document-capture/components/document-capture-warning.tsx
index c7944a15eb5..80347039384 100644
--- a/app/javascript/packages/document-capture/components/document-capture-warning.tsx
+++ b/app/javascript/packages/document-capture/components/document-capture-warning.tsx
@@ -2,6 +2,7 @@ import { Cancel } from '@18f/identity-verify-flow';
import { useI18n, HtmlTextWithStrongNoWrap } from '@18f/identity-react-i18n';
import { useContext, useEffect, useRef } from 'react';
import { FormStepError } from '@18f/identity-form-steps';
+import type { I18n } from '@18f/identity-i18n';
import Warning from './warning';
import DocumentCaptureTroubleshootingOptions from './document-capture-troubleshooting-options';
import UnknownError from './unknown-error';
@@ -11,6 +12,7 @@ import AnalyticsContext from '../context/analytics';
interface DocumentCaptureWarningProps {
isFailedDocType: boolean;
isFailedResult: boolean;
+ isFailedSelfieLivenessOrQuality: boolean;
remainingAttempts: number;
actionOnClick?: () => void;
unknownFieldErrors: FormStepError<{ front: string; back: string }>[];
@@ -19,9 +21,25 @@ interface DocumentCaptureWarningProps {
const DISPLAY_ATTEMPTS = 3;
+type GetHeadingArguments = {
+ isFailedDocType: boolean;
+ isFailedSelfieLivenessOrQuality: boolean;
+ t: typeof I18n.prototype.t;
+};
+function getHeading({ isFailedDocType, isFailedSelfieLivenessOrQuality, t }: GetHeadingArguments) {
+ if (isFailedDocType) {
+ return t('errors.doc_auth.doc_type_not_supported_heading');
+ }
+ if (isFailedSelfieLivenessOrQuality) {
+ return t('errors.doc_auth.selfie_not_live_or_poor_quality_heading');
+ }
+ return t('errors.doc_auth.rate_limited_heading');
+}
+
function DocumentCaptureWarning({
isFailedDocType,
isFailedResult,
+ isFailedSelfieLivenessOrQuality,
remainingAttempts,
actionOnClick,
unknownFieldErrors = [],
@@ -32,15 +50,13 @@ function DocumentCaptureWarning({
const { trackEvent } = useContext(AnalyticsContext);
const nonIppOrFailedResult = !inPersonURL || isFailedResult;
- const heading = isFailedDocType
- ? t('errors.doc_auth.doc_type_not_supported_heading')
- : t('errors.doc_auth.rate_limited_heading');
+ const heading = getHeading({ isFailedDocType, isFailedSelfieLivenessOrQuality, t });
const actionText = nonIppOrFailedResult
? t('idv.failure.button.warning')
: t('idv.failure.button.try_online');
- const subheading = !nonIppOrFailedResult && !isFailedDocType && (
- {t('errors.doc_auth.rate_limited_subheading')}
- );
+ const subheading = !nonIppOrFailedResult &&
+ !isFailedDocType &&
+ !isFailedSelfieLivenessOrQuality && {t('errors.doc_auth.rate_limited_subheading')}
;
const subheadingRef = useRef(null);
const errorMessageDisplayedRef = useRef(null);
@@ -79,6 +95,7 @@ function DocumentCaptureWarning({
unknownFieldErrors={unknownFieldErrors}
remainingAttempts={remainingAttempts}
isFailedDocType={isFailedDocType}
+ isFailedSelfieLivenessOrQuality={isFailedSelfieLivenessOrQuality}
hasDismissed={hasDismissed}
/>
diff --git a/app/javascript/packages/document-capture/components/document-capture.tsx b/app/javascript/packages/document-capture/components/document-capture.tsx
index c591d0e3472..cb2f3d813e5 100644
--- a/app/javascript/packages/document-capture/components/document-capture.tsx
+++ b/app/javascript/packages/document-capture/components/document-capture.tsx
@@ -115,6 +115,8 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) {
remainingAttempts: submissionError.remainingAttempts,
isFailedResult: submissionError.isFailedResult,
isFailedDocType: submissionError.isFailedDocType,
+ isFailedSelfieLivenessOrQuality:
+ submissionError.selfieNotLive || submissionError.selfieNotGoodQuality,
captureHints: submissionError.hints,
pii: submissionError.pii,
failedImageFingerprints: submissionError.failed_image_fingerprints,
diff --git a/app/javascript/packages/document-capture/components/review-issues-step.tsx b/app/javascript/packages/document-capture/components/review-issues-step.tsx
index 82e777548f7..5e2c7f82e2e 100644
--- a/app/javascript/packages/document-capture/components/review-issues-step.tsx
+++ b/app/javascript/packages/document-capture/components/review-issues-step.tsx
@@ -39,6 +39,7 @@ interface ReviewIssuesStepProps extends FormStepComponentProps {
unknownFieldErrors: FormStepError<{ front: string; back: string }>[];
isFailedDocType: boolean;
+ isFailedSelfieLivenessOrQuality: boolean;
remainingAttempts: number;
altFailedDocTypeMsg?: string | null;
+ altIsFailedSelfieDontIncludeAttempts?: boolean;
hasDismissed: boolean;
}
@@ -24,11 +26,27 @@ function formatIdTypeMsg({ altFailedDocTypeMsg, acceptedIdUrl }) {
});
}
+type GetErrorArguments = {
+ unknownFieldErrors: FormStepError<{ front: string; back: string }>[];
+};
+function getError({ unknownFieldErrors }: GetErrorArguments) {
+ const errs =
+ !!unknownFieldErrors &&
+ // Errors where the field than is not 'front' or 'back'. In practice this means the field
+ // should be from the 'general' field in the "IdV: doc auth image upload vendor submitted" event
+ unknownFieldErrors.filter((error) => !['front', 'back'].includes(error.field!));
+ const err = errs.length !== 0 ? errs[0].error : null;
+
+ return err;
+}
+
function UnknownError({
unknownFieldErrors = [],
isFailedDocType = false,
+ isFailedSelfieLivenessOrQuality = false,
remainingAttempts,
altFailedDocTypeMsg = null,
+ altIsFailedSelfieDontIncludeAttempts = false,
hasDismissed,
}: UnknownErrorProps) {
const { t } = useI18n();
@@ -45,10 +63,8 @@ function UnknownError({
location: 'document_capture_review_issues',
});
- const errs =
- !!unknownFieldErrors &&
- unknownFieldErrors.filter((error) => !['front', 'back'].includes(error.field!));
- const err = errs.length !== 0 ? errs[0].error : null;
+ const err = getError({ unknownFieldErrors });
+
if (isFailedDocType && !!altFailedDocTypeMsg) {
return (
{formatIdTypeMsg({ altFailedDocTypeMsg, acceptedIdUrl })}
@@ -64,6 +80,20 @@ function UnknownError({
);
}
+ if (isFailedSelfieLivenessOrQuality && err) {
+ return (
+ <>
+ {err.message}
+
+ {!altIsFailedSelfieDontIncludeAttempts && (
+
+ )}
+
+ >
+ );
+ }
if (err && !hasDismissed) {
return {err.message}
;
}
diff --git a/app/javascript/packages/document-capture/context/upload.tsx b/app/javascript/packages/document-capture/context/upload.tsx
index 6f6879cfee7..5a80aa66c1d 100644
--- a/app/javascript/packages/document-capture/context/upload.tsx
+++ b/app/javascript/packages/document-capture/context/upload.tsx
@@ -100,6 +100,16 @@ export interface UploadErrorResponse {
*/
doc_type_supported: boolean;
+ /*
+ * Whether the selfie passed the liveness check from trueid
+ */
+ selfie_live?: boolean;
+
+ /*
+ * Whether the selfie passed the quality check from trueid.
+ */
+ selfie_quality_good?: boolean;
+
/**
* Record of failed image fingerprints
*/
diff --git a/app/javascript/packages/document-capture/services/upload.ts b/app/javascript/packages/document-capture/services/upload.ts
index ed0c77e16ed..f124210d35d 100644
--- a/app/javascript/packages/document-capture/services/upload.ts
+++ b/app/javascript/packages/document-capture/services/upload.ts
@@ -42,6 +42,10 @@ export class UploadFormEntriesError extends FormError {
isFailedDocType = false;
+ selfieNotLive = false;
+
+ selfieNotGoodQuality = false;
+
pii?: PII;
hints = false;
@@ -124,6 +128,11 @@ const upload: UploadImplementation = async function (payload, { method = 'POST',
error.isFailedDocType = !result.doc_type_supported;
+ error.selfieNotLive = result.selfie_live === undefined ? false : !result.selfie_live;
+
+ error.selfieNotGoodQuality =
+ result.selfie_quality_good === undefined ? false : !result.selfie_quality_good;
+
error.failed_image_fingerprints = result.failed_image_fingerprints ?? { front: [], back: [] };
throw error;
diff --git a/app/services/doc_auth/errors.rb b/app/services/doc_auth/errors.rb
index 98068b64158..a8bdf9c514a 100644
--- a/app/services/doc_auth/errors.rb
+++ b/app/services/doc_auth/errors.rb
@@ -29,6 +29,7 @@ module Errors
SELFIE_FAILURE = 'selfie_failure'
SELFIE_NOT_LIVE = 'selfie_not_live'
SELFIE_POOR_QUALITY = 'selfie_poor_quality'
+ SELFIE_NOT_LIVE_POOR_QUALITY_FIELD = 'selfie_not_live_poor_quality'
SEX_CHECK = 'sex_check'
VISIBLE_COLOR_CHECK = 'visible_color_check'
VISIBLE_PHOTO_CHECK = 'visible_photo_check'
@@ -120,8 +121,8 @@ module Errors
# TODO, theses messages need modifying
# Liveness, use general error for now
SELFIE_FAILURE => { long_msg: GENERAL_ERROR, field_msg: FALLBACK_FIELD_LEVEL, hints: false },
- SELFIE_NOT_LIVE => { long_msg: GENERAL_ERROR, field_msg: FALLBACK_FIELD_LEVEL, hints: false },
- SELFIE_POOR_QUALITY => { long_msg: GENERAL_ERROR, field_msg: FALLBACK_FIELD_LEVEL, hints: false },
+ SELFIE_NOT_LIVE => { long_msg: SELFIE_NOT_LIVE, field_msg: SELFIE_NOT_LIVE_POOR_QUALITY_FIELD, hints: false },
+ SELFIE_POOR_QUALITY => { long_msg: SELFIE_POOR_QUALITY, field_msg: SELFIE_NOT_LIVE_POOR_QUALITY_FIELD, hints: false },
}
# rubocop:enable Layout/LineLength
end
diff --git a/app/services/doc_auth/selfie_concern.rb b/app/services/doc_auth/selfie_concern.rb
index 43f29dec079..ec8eab06ced 100644
--- a/app/services/doc_auth/selfie_concern.rb
+++ b/app/services/doc_auth/selfie_concern.rb
@@ -4,25 +4,25 @@ module SelfieConcern
def selfie_live?
portait_error = get_portrait_error(portrait_match_results)
return true if portait_error.nil? || portait_error.blank?
- return error_is_not_live(portait_error)
+ return !error_is_not_live(portait_error)
end
def selfie_quality_good?
portait_error = get_portrait_error(portrait_match_results)
return true if portait_error.nil? || portait_error.blank?
- return error_is_poor_quality(portait_error)
+ return !error_is_poor_quality(portait_error)
end
def error_is_success(error_message)
- return error_message != ERROR_TEXTS[:success]
+ error_message == ERROR_TEXTS[:success]
end
def error_is_not_live(error_message)
- return error_message != ERROR_TEXTS[:not_live]
+ return error_message == ERROR_TEXTS[:not_live]
end
def error_is_poor_quality(error_message)
- return error_message != ERROR_TEXTS[:poor_quality]
+ error_message == ERROR_TEXTS[:poor_quality]
end
private
diff --git a/app/services/doc_auth_router.rb b/app/services/doc_auth_router.rb
index b5f380ce177..80f934c221e 100644
--- a/app/services/doc_auth_router.rb
+++ b/app/services/doc_auth_router.rb
@@ -54,6 +54,13 @@ module DocAuthRouter
# i18n-tasks-use t('doc_auth.errors.alerts.ref_control_number_check')
DocAuth::Errors::REF_CONTROL_NUMBER_CHECK =>
'doc_auth.errors.alerts.ref_control_number_check',
+ # i18n-tasks-use t('doc_auth.errors.alerts.selfie_not_live')
+ DocAuth::Errors::SELFIE_NOT_LIVE => 'doc_auth.errors.alerts.selfie_not_live',
+ # i18n-tasks-use t('doc_auth.errors.alerts.selfie_poor_quality')
+ DocAuth::Errors::SELFIE_POOR_QUALITY => 'doc_auth.errors.alerts.selfie_poor_quality',
+ # i18n-tasks-use t('doc_auth.errors.alerts.selfie_not_live_poor_quality')
+ DocAuth::Errors::SELFIE_NOT_LIVE_POOR_QUALITY_FIELD =>
+ 'doc_auth.errors.alerts.selfie_not_live_poor_quality',
# i18n-tasks-use t('doc_auth.errors.alerts.sex_check')
DocAuth::Errors::SEX_CHECK => 'doc_auth.errors.alerts.sex_check',
# i18n-tasks-use t('doc_auth.errors.alerts.visible_color_check')
diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml
index 40b0f6a3a9a..788b09b34ba 100644
--- a/config/locales/doc_auth/en.yml
+++ b/config/locales/doc_auth/en.yml
@@ -38,6 +38,11 @@ en:
the picture. Try taking new pictures.
issue_date_checks: We couldn’t read the issue date on your ID. Try taking new pictures.
ref_control_number_check: We couldn’t read the control number barcode. Try taking new pictures.
+ selfie_not_live: 'Try taking a photo of yourself again. Make sure your whole
+ face is clear and visible in the photo.'
+ selfie_not_live_poor_quality: 'We couldn’t verify the photo of yourself. Try taking a new picture.'
+ selfie_poor_quality: 'Try taking a photo of yourself again. Make sure your whole
+ face is clear and visible in the photo.'
sex_check: We couldn’t read the sex on your ID. Try taking new pictures.
visible_color_check: We couldn’t verify your ID. It might have moved when you
took the picture, or the picture is too dark. Try taking new pictures
diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml
index ce537f99f04..122fd64e7d4 100644
--- a/config/locales/doc_auth/es.yml
+++ b/config/locales/doc_auth/es.yml
@@ -48,6 +48,11 @@ es:
identidad. Intente tomar nuevas fotos.
ref_control_number_check: No pudimos leer el código de barras del número de
control. Intente tomar nuevas fotografías.
+ selfie_not_live: 'Intenta volver a tomarte una foto. Asegúrate de que tu rostro
+ completo esté claro y visible en la foto.'
+ selfie_not_live_poor_quality: 'No pudimos verificar su foto. Trate de tomarse otra foto.'
+ selfie_poor_quality: 'Intenta volver a tomarte una foto. Asegúrate de que tu
+ rostro completo esté claro y visible en la foto.'
sex_check: No pudimos leer el sexo en su documento de identidad. Intente tomar
nuevas fotos.
visible_color_check: No pudimos verificar su documento de identidad. Puede que
diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml
index 1c12cc52be6..67298dcd2c2 100644
--- a/config/locales/doc_auth/fr.yml
+++ b/config/locales/doc_auth/fr.yml
@@ -51,6 +51,12 @@ fr:
d’identité. Essayez de prendre de nouvelles photos.
ref_control_number_check: Nous n’avons pas pu lire le code-barres du numéro de
contrôle. Essayez de prendre de nouvelles photos.
+ selfie_not_live: 'Rééssayez de vous prendre en photo. Assurez-vous que lensemble
+ de votre visage est clair et visible sur la photo.'
+ selfie_not_live_poor_quality: 'Nous n’avons pas réussi à vérifier votre photo.
+ Essayez à nouveau avec une nouvelle photo.'
+ selfie_poor_quality: 'Rééssayez de vous prendre en photo. Assurez-vous que
+ l’ensemble de votre visage est clair et visible sur la photo.'
sex_check: Nous n’avons pas pu lire le sexe sur votre pièce d’identité. Essayez
de prendre de nouvelles photos.
visible_color_check: Nous n’avons pas pu vérifier votre pièce d’identité. Elle a
diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml
index d5798e70027..f95ad00e9d6 100644
--- a/config/locales/errors/en.yml
+++ b/config/locales/errors/en.yml
@@ -34,6 +34,7 @@ en:
rate_limited_text_html: 'For your security, we limit the number of times you can
attempt to verify a document online. Try again in
%{timeout}.'
+ selfie_not_live_or_poor_quality_heading: We could not verify the photo of yourself
send_link_limited: You tried too many times, please try again in %{timeout}. You
can also go back and choose to use your computer instead.
enter_code:
diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml
index 970491697c4..69149b91f0b 100644
--- a/config/locales/errors/es.yml
+++ b/config/locales/errors/es.yml
@@ -36,6 +36,7 @@ es:
rate_limited_text_html: 'Por su seguridad, limitamos el número de veces que
puede intentar verificar un documento en línea. Inténtelo de
nuevo en %{timeout}.'
+ selfie_not_live_or_poor_quality_heading: No pudimos verificar tu foto
send_link_limited: Ha intentado demasiadas veces, por favor, inténtelo de nuevo
en %{timeout}. También puede retroceder y elegir utilizar su computadora
como alternativa.
diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml
index a62e9a84798..94656f325cc 100644
--- a/config/locales/errors/fr.yml
+++ b/config/locales/errors/fr.yml
@@ -40,6 +40,7 @@ fr:
rate_limited_text_html: 'Pour votre sécurité, nous limitons le nombre de fois où
vous pouvez tenter de vérifier un document en ligne. Veuillez
réessayer dans %{timeout}.'
+ selfie_not_live_or_poor_quality_heading: Nous n’avons pas pu vérifier votre photo
send_link_limited: Vous avez essayé trop de fois, veuillez réessayer dans
%{timeout}. Vous pouvez également revenir en arrière et choisir
d’utiliser votre ordinateur à la place.
diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb
index 340aa1fdbb4..a84172e862d 100644
--- a/spec/features/idv/doc_auth/document_capture_spec.rb
+++ b/spec/features/idv/doc_auth/document_capture_spec.rb
@@ -250,6 +250,84 @@
end
end
+ context 'selfie with no liveness or poor quality is uploaded', allow_browser_log: true do
+ it 'try again and page show no liveness inline error message' do
+ visit_idp_from_oidc_sp_with_ial2
+ sign_in_and_2fa_user(user)
+ complete_doc_auth_steps_before_document_capture_step
+ attach_images(
+ Rails.root.join(
+ 'spec', 'fixtures',
+ 'ial2_test_credential_no_liveness.yml'
+ ),
+ )
+ attach_selfie(
+ Rails.root.join(
+ 'spec', 'fixtures',
+ 'ial2_test_credential_no_liveness.yml'
+ ),
+ )
+ submit_images
+ message = strip_tags(t('errors.doc_auth.selfie_not_live_or_poor_quality_heading'))
+ expect(page).to have_content(message)
+ detail_message = strip_tags(t('doc_auth.errors.alerts.selfie_not_live'))
+ security_message = strip_tags(
+ t(
+ 'idv.warning.attempts_html',
+ count: IdentityConfig.store.doc_auth_max_attempts - 1,
+ ),
+ )
+ expect(page).to have_content(detail_message << "\n" << security_message)
+ review_issues_header = strip_tags(
+ t('errors.doc_auth.selfie_not_live_or_poor_quality_heading'),
+ )
+ expect(page).to have_content(review_issues_header)
+ expect(page).to have_current_path(idv_document_capture_path)
+ click_try_again
+ expect(page).to have_current_path(idv_document_capture_path)
+ inline_error = strip_tags(t('doc_auth.errors.alerts.selfie_not_live_poor_quality'))
+ expect(page).to have_content(inline_error)
+ end
+
+ it 'try again and page show poor quality inline error message' do
+ visit_idp_from_oidc_sp_with_ial2
+ sign_in_and_2fa_user(user)
+ complete_doc_auth_steps_before_document_capture_step
+ attach_images(
+ Rails.root.join(
+ 'spec', 'fixtures',
+ 'ial2_test_credential_poor_quality.yml'
+ ),
+ )
+ attach_selfie(
+ Rails.root.join(
+ 'spec', 'fixtures',
+ 'ial2_test_credential_poor_quality.yml'
+ ),
+ )
+ submit_images
+ message = strip_tags(t('errors.doc_auth.selfie_not_live_or_poor_quality_heading'))
+ expect(page).to have_content(message)
+ detail_message = strip_tags(t('doc_auth.errors.alerts.selfie_poor_quality'))
+ security_message = strip_tags(
+ t(
+ 'idv.warning.attempts_html',
+ count: IdentityConfig.store.doc_auth_max_attempts - 1,
+ ),
+ )
+ expect(page).to have_content(detail_message << "\n" << security_message)
+ review_issues_header = strip_tags(
+ t('errors.doc_auth.selfie_not_live_or_poor_quality_heading'),
+ )
+ expect(page).to have_content(review_issues_header)
+ expect(page).to have_current_path(idv_document_capture_path)
+ click_try_again
+ expect(page).to have_current_path(idv_document_capture_path)
+ inline_error = strip_tags(t('doc_auth.errors.alerts.selfie_not_live_poor_quality'))
+ expect(page).to have_content(inline_error)
+ end
+ end
+
context 'when selfie check is not enabled (flag off, and/or in production)' do
let(:selfie_check_enabled) { false }
it 'proceeds to the next page with valid info, excluding a selfie image' do
diff --git a/spec/fixtures/ial2_test_credential_no_liveness.yml b/spec/fixtures/ial2_test_credential_no_liveness.yml
new file mode 100644
index 00000000000..850f6a33ca8
--- /dev/null
+++ b/spec/fixtures/ial2_test_credential_no_liveness.yml
@@ -0,0 +1,20 @@
+portrait_match_results:
+ # returns the portrait match result
+ FaceMatchResult: Fail
+ # returns the liveness result
+ FaceErrorMessage: 'Liveness: NotLive'
+doc_auth_result: Passed
+document:
+ first_name: Jane
+ last_name: Doe
+ middle_name: Q
+ address1: 1800 F Street
+ address2: Apt 3
+ city: Bayside
+ state: NY
+ zipcode: '11364'
+ dob: 10/06/1938
+ phone: +1 314-555-1212
+ state_id_jurisdiction: 'ND'
+ state_id_number: 'S59397998'
+ state_id_type: drivers_license
diff --git a/spec/fixtures/ial2_test_credential_poor_quality.yml b/spec/fixtures/ial2_test_credential_poor_quality.yml
new file mode 100644
index 00000000000..11519ae47b5
--- /dev/null
+++ b/spec/fixtures/ial2_test_credential_poor_quality.yml
@@ -0,0 +1,20 @@
+portrait_match_results:
+ # returns the portrait match result
+ FaceMatchResult: Fail
+ # returns the liveness result
+ FaceErrorMessage: 'Liveness: PoorQuality'
+doc_auth_result: Passed
+document:
+ first_name: Jane
+ last_name: Doe
+ middle_name: Q
+ address1: 1800 F Street
+ address2: Apt 3
+ city: Bayside
+ state: NY
+ zipcode: '11364'
+ dob: 10/06/1938
+ phone: +1 314-555-1212
+ state_id_jurisdiction: 'ND'
+ state_id_number: 'S59397998'
+ state_id_type: drivers_license
diff --git a/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb b/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb
index c39d2435a1d..a22023c982a 100644
--- a/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb
+++ b/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb
@@ -343,7 +343,9 @@
errors = post_images_response.errors
expect(errors.keys).to contain_exactly(:general, :hints, :selfie)
- expect(errors[:selfie]).to contain_exactly(DocAuth::Errors::FALLBACK_FIELD_LEVEL)
+ expect(errors[:selfie]).to contain_exactly(
+ DocAuth::Errors::SELFIE_NOT_LIVE_POOR_QUALITY_FIELD,
+ )
end
end
@@ -373,7 +375,9 @@
errors = post_images_response.errors
expect(errors.keys).to contain_exactly(:general, :hints, :selfie)
- expect(errors[:selfie]).to contain_exactly(DocAuth::Errors::FALLBACK_FIELD_LEVEL)
+ expect(errors[:selfie]).to contain_exactly(
+ DocAuth::Errors::SELFIE_NOT_LIVE_POOR_QUALITY_FIELD,
+ )
end
end
From 250625eb8d235b06deea9d49cd1b7543aa417162 Mon Sep 17 00:00:00 2001
From: "Luis H. Matos"
Date: Wed, 31 Jan 2024 16:01:02 -0600
Subject: [PATCH 03/25] Lg 12117 summarize multiple apps (#10008)
* LG-12117 Condense Reuse Rate Report
changelog: Internal, Reporting, Condensing Reuse Rate Report rows
---
.../reporting/account_reuse_report.rb | 26 ++-
.../reporting/account_reuse_report_spec.rb | 171 +++++++++++++++---
2 files changed, 165 insertions(+), 32 deletions(-)
diff --git a/app/services/reporting/account_reuse_report.rb b/app/services/reporting/account_reuse_report.rb
index 4bb534270c5..f8c08b7ad65 100644
--- a/app/services/reporting/account_reuse_report.rb
+++ b/app/services/reporting/account_reuse_report.rb
@@ -65,15 +65,20 @@ def initialize(
def update_details(
num_entities: nil, entity_type: nil,
- num_idv_users: nil, num_all_users: nil
+ num_all_users: nil, all_percent: nil,
+ num_idv_users: nil, idv_percent: nil
)
self.num_entities = num_entities if !num_entities.nil?
self.entity_type = entity_type if !entity_type.nil?
+ self.num_all_users = num_all_users if !num_all_users.nil?
+
+ self.all_percent = all_percent if !all_percent.nil?
+
self.num_idv_users = num_idv_users if !num_idv_users.nil?
- self.num_all_users = num_all_users if !num_all_users.nil?
+ self.idv_percent = idv_percent if !idv_percent.nil?
self
end
@@ -173,6 +178,23 @@ def update_from_results(results:, total_registered:, total_proofed:)
end
end
end
+
+ results.each_with_index do |details_section, section_index|
+ details_section.select { |details| details.num_entities >= 10 }.
+ reduce do |summary_row, captured_row|
+ # Delete any rows after the first captured_row (which becomes the summary_row)
+ details_section.delete(captured_row) if captured_row != summary_row
+ summary_row.update_details(
+ num_entities: "10-#{captured_row.num_entities}",
+ entity_type: summary_row.entity_type,
+ num_all_users: summary_row.num_all_users + captured_row.num_all_users,
+ all_percent: summary_row.all_percent + captured_row.all_percent,
+ num_idv_users: summary_row.num_idv_users + captured_row.num_idv_users,
+ idv_percent: summary_row.idv_percent + captured_row.idv_percent,
+ )
+ end
+ end
+
self.details_section = results
self
diff --git a/spec/services/reporting/account_reuse_report_spec.rb b/spec/services/reporting/account_reuse_report_spec.rb
index 7c399430e44..4afc8d19e1f 100644
--- a/spec/services/reporting/account_reuse_report_spec.rb
+++ b/spec/services/reporting/account_reuse_report_spec.rb
@@ -14,12 +14,25 @@
let(:in_query) { report_date - 12.days }
let(:out_of_query) { report_date + 12.days }
- let(:agency) { create(:agency, name: 'The Agency') }
+ let(:agency1) { create(:agency, name: 'The Agency') }
let(:agency2) { create(:agency, name: 'The Other Agency') }
let(:sp_a) { 'a' }
let(:sp_b) { 'b' }
let(:sp_c) { 'c' }
let(:sp_d) { 'd' }
+ let(:sp_e) { 'e' }
+ let(:sp_f) { 'f' }
+ let(:sp_g) { 'g' }
+ let(:sp_h) { 'h' }
+ let(:sp_i) { 'i' }
+ let(:sp_j) { 'j' }
+ let(:sp_k) { 'k' }
+ let(:sp_l) { 'l' }
+
+ let(:agency1_apps) { [sp_a, sp_d, sp_e, sp_h, sp_i, sp_l] }
+ let(:all_agency_apps) do
+ [sp_a, sp_b, sp_c, sp_d, sp_e, sp_f, sp_g, sp_h, sp_i, sp_j, sp_k, sp_l]
+ end
before do
create(
@@ -27,7 +40,7 @@
issuer: sp_a,
iaa: 'iaa123',
friendly_name: 'The App',
- agency: agency,
+ agency: agency1,
)
create(
:service_provider,
@@ -48,7 +61,63 @@
issuer: sp_d,
iaa: 'iaa321',
friendly_name: 'The Other First App',
- agency: agency,
+ agency: agency1,
+ )
+ create(
+ :service_provider,
+ issuer: sp_e,
+ iaa: 'iaa123',
+ friendly_name: 'App E',
+ agency: agency1,
+ )
+ create(
+ :service_provider,
+ issuer: sp_f,
+ iaa: 'iaa456',
+ friendly_name: 'App F',
+ agency: agency2,
+ )
+ create(
+ :service_provider,
+ issuer: sp_g,
+ iaa: 'iaa789',
+ friendly_name: 'App G',
+ agency: agency2,
+ )
+ create(
+ :service_provider,
+ issuer: sp_h,
+ iaa: 'iaa321',
+ friendly_name: 'App H',
+ agency: agency1,
+ )
+ create(
+ :service_provider,
+ issuer: sp_i,
+ iaa: 'iaa123',
+ friendly_name: 'App I',
+ agency: agency1,
+ )
+ create(
+ :service_provider,
+ issuer: sp_j,
+ iaa: 'iaa456',
+ friendly_name: 'App J',
+ agency: agency2,
+ )
+ create(
+ :service_provider,
+ issuer: sp_k,
+ iaa: 'iaa789',
+ friendly_name: 'App K',
+ agency: agency2,
+ )
+ create(
+ :service_provider,
+ issuer: sp_l,
+ iaa: 'iaa321',
+ friendly_name: 'App L',
+ agency: agency1,
)
# Seed the database with data to be queried
@@ -61,57 +130,72 @@
# User 6 has 2 SPs and only 1 shows up in the query
# User 7 has 1 SP and 1 shows up in the query
# User 8 has 1 SP and 0 show up in the query
+ # User 9 has 2 SPs and only 1 shows up in the query
+ # User 10 has 2 SPs and 0 show up in the query
+ # User 11 has 2 SPs and 0 show up in the query
+ # User 12 has 1 SP and 1 shows up in the query
+ # User 13 has 1 SP and 0 show up in the query
+ # User 14 has 1 SP and 0 show up in the query
+ # User 15 has 12 SPs and 12 show up in the query
+ # User 16 has 11 SPs and 11 show up in the query
+ # User 17 has 11 SPs and 11 show up in the query
+ # User 18 has 10 SPs and 10 show up in the query
+ # User 19 has 10 SPs and 10 show up in the query
+ # User 20 has 10 SPs and 9 show up in the query
#
# This will give 1 user with 3 SPs/apps and 3 users with 2 SPs/apps for the IDV app report
# This will give 4 users with 3 SPs/apps and 5 users with 2 SPs/apps for the ALL app report
- # This will give 3 users with 2 agencies for the IDV agency report
- # This will give 7 users with 2 agencies for the ALL agency report
+ # This will give 1 user with 9 SPs/apps for the ALL app report and 0 for the IDV app report
+ # This will give 6 users with 10-12 SPs/apps for the ALL app report
+ # This will give 5 users with 10-12 SPs/apps for the IDV app report
+ # This will give 9 users with 2 agencies for the IDV agency report
+ # This will give 13 users with 2 agencies for the ALL agency report
users_to_query = [
{ id: 1, # 3 apps, 2 agencies
created_timestamp: in_query,
- sp: [sp_a, sp_b, sp_c],
- sp_timestamp: [in_query, in_query, in_query] },
+ sp: all_agency_apps.first(3),
+ sp_timestamp: Array.new(3) { in_query } },
{ id: 2, # 3 apps, 2 agencies
created_timestamp: in_query,
- sp: [sp_a, sp_b, sp_c],
+ sp: all_agency_apps.first(3),
sp_timestamp: [in_query, in_query, out_of_query] },
{ id: 3, # 3 apps, 2 agencies
created_timestamp: in_query,
- sp: [sp_a, sp_b, sp_c],
+ sp: all_agency_apps.first(3),
sp_timestamp: [in_query, out_of_query, out_of_query] },
{ id: 4, # 3 apps, 2 agencies
created_timestamp: in_query,
- sp: [sp_a, sp_b, sp_c],
+ sp: all_agency_apps.first(3),
sp_timestamp: [in_query, out_of_query, out_of_query] },
{ id: 5, # 3 apps, 2 agencies
created_timestamp: out_of_query,
- sp: [sp_a, sp_b, sp_c],
- sp_timestamp: [out_of_query, out_of_query, out_of_query] },
+ sp: all_agency_apps.first(3),
+ sp_timestamp: Array.new(3) { out_of_query } },
{ id: 6, # 2 apps, 2 agencies
created_timestamp: in_query,
- sp: [sp_a, sp_b],
- sp_timestamp: [in_query, in_query] },
+ sp: all_agency_apps.first(2),
+ sp_timestamp: Array.new(2) { in_query } },
{ id: 7, # 2 apps, 1 agency
created_timestamp: in_query,
- sp: [sp_a, sp_d],
- sp_timestamp: [in_query, in_query] },
+ sp: agency1_apps.first(2),
+ sp_timestamp: Array.new(2) { in_query } },
{ id: 8, # 2 apps, 2 agencies
created_timestamp: in_query,
- sp: [sp_a, sp_b],
+ sp: all_agency_apps.first(2),
sp_timestamp: [in_query, out_of_query] },
{ id: 9, # 2 apps, 1 agency
created_timestamp: in_query,
- sp: [sp_a, sp_d],
+ sp: agency1_apps.first(2),
sp_timestamp: [in_query, out_of_query] },
{ id: 10, # 2 apps, 2 agencies
created_timestamp: in_query,
- sp: [sp_a, sp_b],
- sp_timestamp: [out_of_query, out_of_query] },
+ sp: all_agency_apps.first(2),
+ sp_timestamp: Array.new(2) { out_of_query } },
{ id: 11, # 2 apps, 2 agencies
created_timestamp: out_of_query,
- sp: [sp_a, sp_b],
- sp_timestamp: [out_of_query, out_of_query] },
+ sp: all_agency_apps.first(2),
+ sp_timestamp: Array.new(2) { out_of_query } },
{ id: 12,
created_timestamp: in_query,
sp: [sp_a],
@@ -124,6 +208,31 @@
created_timestamp: out_of_query,
sp: [sp_a],
sp_timestamp: [out_of_query] },
+ { id: 15, # 12 apps, 2 agencies
+ created_timestamp: in_query,
+ sp: all_agency_apps,
+ sp_timestamp: Array.new(12) { in_query } },
+ { id: 16, # 11 apps, 2 agencies
+ created_timestamp: in_query,
+ sp: all_agency_apps.first(11),
+ sp_timestamp: Array.new(11) { in_query } },
+ { id: 17, # 11 apps, 2 agencies
+ created_timestamp: in_query,
+ sp: all_agency_apps.first(11),
+ sp_timestamp: Array.new(11) { in_query } },
+ { id: 18, # 10 apps, 2 agencies
+ created_timestamp: in_query,
+ sp: all_agency_apps.first(10),
+ sp_timestamp: Array.new(10) { in_query } },
+ { id: 19, # 10 apps, 2 agencies
+ created_timestamp: in_query,
+ sp: all_agency_apps.first(10),
+ sp_timestamp: Array.new(10) { in_query } },
+ { id: 20, # 10 apps, 2 agencies
+ created_timestamp: in_query,
+ sp: all_agency_apps.first(10),
+ sp_timestamp: Array.new(9) { in_query } + Array.new(1) { out_of_query } },
+
]
users_to_query.each do |user|
@@ -139,8 +248,8 @@
end
# Create active profiles for total_proofed_identities
- # These 13 profiles will yield 10 active profiles in the results
- (1..10).each do |_|
+ # These 20 profiles will yield 10 active profiles in the results
+ 10.times do
create(
:profile,
:active,
@@ -148,7 +257,7 @@
user: create(:user, :fully_registered, registered_at: in_query),
)
end
- (1..3).each do |_|
+ 10.times do
create(
:profile,
:active,
@@ -162,11 +271,13 @@
it 'has the correct results' do
expected_csv = [
['Metric', 'Num. all users', '% of accounts', 'Num. IDV users', '% of accounts'],
- ['2 apps', 5, 5 / 13.0, 3, 0.3],
- ['3 apps', 4, 4 / 13.0, 1, 0.1],
- ['2+ apps', 9, 9 / 13.0, 4, 0.4],
- ['2 agencies', 7, 7 / 13.0, 3, 0.3],
- ['2+ agencies', 7, 7 / 13.0, 3, 0.3],
+ ['2 apps', 5, 5 / 20.0, 3, 0.3],
+ ['3 apps', 4, 4 / 20.0, 1, 0.1],
+ ['9 apps', 0, 0 / 20.0, 1, 0.1],
+ ['10-12 apps', 6, 6 / 20.0, 5, 0.5],
+ ['2+ apps', 15, 15 / 20.0, 10, 0.9999999999999999],
+ ['2 agencies', 13, 13 / 20.0, 9, 0.9],
+ ['2+ agencies', 13, 13 / 20.0, 9, 0.9],
]
aggregate_failures do
From eb7aa033a183f3792ff3031e933626197869a7b5 Mon Sep 17 00:00:00 2001
From: Andrew Duthie <1779930+aduth@users.noreply.github.com>
Date: Thu, 1 Feb 2024 08:52:17 -0500
Subject: [PATCH 04/25] Silently ignore invalid params for FormSteps (#10005)
* Silently ignore invalid params for FormSteps
changelog: Internal, Document Capture, Refactor handling for invalid URL steps
* Remove skipnav bypass for FormSteps interop
---
.../packages/form-steps/form-steps.spec.tsx | 6 ---
.../packages/form-steps/form-steps.tsx | 3 +-
.../form-steps/use-history-param.spec.tsx | 49 +++++++++++++++++--
.../packages/form-steps/use-history-param.ts | 8 ++-
app/javascript/packs/application.ts | 5 --
5 files changed, 54 insertions(+), 17 deletions(-)
diff --git a/app/javascript/packages/form-steps/form-steps.spec.tsx b/app/javascript/packages/form-steps/form-steps.spec.tsx
index 6877d2c6d2e..cf883d4eac1 100644
--- a/app/javascript/packages/form-steps/form-steps.spec.tsx
+++ b/app/javascript/packages/form-steps/form-steps.spec.tsx
@@ -355,12 +355,6 @@ describe('FormSteps', () => {
expect(window.location.hash).to.equal('#second');
});
- it('resets hash in URL if there is no matching step', () => {
- window.location.hash = '#example';
- render();
- expect(window.location.hash).to.equal('');
- });
-
it('syncs step by history events', async () => {
const { getByText, findByText, getByLabelText } = render();
diff --git a/app/javascript/packages/form-steps/form-steps.tsx b/app/javascript/packages/form-steps/form-steps.tsx
index d0c96c7e0e8..f454b303388 100644
--- a/app/javascript/packages/form-steps/form-steps.tsx
+++ b/app/javascript/packages/form-steps/form-steps.tsx
@@ -231,10 +231,11 @@ function FormSteps({
promptOnNavigate = true,
titleFormat,
}: FormStepsProps) {
+ const stepNames = steps.map((step) => step.name);
const [values, setValues] = useState(initialValues);
const [activeErrors, setActiveErrors] = useState(initialActiveErrors);
const formRef = useRef(null as HTMLFormElement | null);
- const [stepName, setStepName] = useHistoryParam(initialStep);
+ const [stepName, setStepName] = useHistoryParam(initialStep, stepNames);
const [stepErrors, setStepErrors] = useState([] as Error[]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [stepCanComplete, setStepCanComplete] = useState(undefined);
diff --git a/app/javascript/packages/form-steps/use-history-param.spec.tsx b/app/javascript/packages/form-steps/use-history-param.spec.tsx
index 94c83bd7061..cf9be56e18d 100644
--- a/app/javascript/packages/form-steps/use-history-param.spec.tsx
+++ b/app/javascript/packages/form-steps/use-history-param.spec.tsx
@@ -1,4 +1,4 @@
-import { render } from '@testing-library/react';
+import { render, act } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import userEvent from '@testing-library/user-event';
import useHistoryParam, { getStepParam } from './use-history-param';
@@ -13,8 +13,14 @@ describe('getStepParam', () => {
});
describe('useHistoryParam', () => {
- function TestComponent({ initialValue }: { initialValue?: string }) {
- const [count = 0, setCount] = useHistoryParam(initialValue);
+ function TestComponent({
+ initialValue,
+ validValues,
+ }: {
+ initialValue?: string;
+ validValues?: string[];
+ }) {
+ const [count = 0, setCount] = useHistoryParam(initialValue, validValues);
return (
<>
@@ -119,4 +125,41 @@ describe('useHistoryParam', () => {
const [path2] = inst2.result.current;
expect(path2).to.equal('root');
});
+
+ context('when specifying valid values', () => {
+ it('syncs by history events for a valid value', async () => {
+ const { getByText, getByDisplayValue, findByDisplayValue } = render(
+ ,
+ );
+ expect(getByDisplayValue('0')).to.be.ok();
+
+ await userEvent.click(getByText('Increment'));
+
+ expect(getByDisplayValue('1')).to.be.ok();
+ expect(window.location.hash).to.equal('#1');
+
+ act(() => {
+ window.history.back();
+ });
+
+ expect(await findByDisplayValue('0')).to.be.ok();
+ expect(window.location.hash).to.equal('');
+ });
+
+ it('maintains value (does not sync) by history events for an invalid value', async () => {
+ const { getByDisplayValue } = render();
+ expect(getByDisplayValue('0')).to.be.ok();
+ const popstateHandled = new Promise((resolve) =>
+ window.addEventListener('popstate', resolve, { once: true }),
+ );
+
+ act(() => {
+ window.location.hash = '#wrong';
+ });
+
+ await popstateHandled;
+ expect(getByDisplayValue('0')).to.be.ok();
+ expect(window.location.hash).to.equal('#wrong');
+ });
+ });
});
diff --git a/app/javascript/packages/form-steps/use-history-param.ts b/app/javascript/packages/form-steps/use-history-param.ts
index 1839840924d..7f91b3609ff 100644
--- a/app/javascript/packages/form-steps/use-history-param.ts
+++ b/app/javascript/packages/form-steps/use-history-param.ts
@@ -29,13 +29,17 @@ const subscribers: Array<() => void> = [];
*/
function useHistoryParam(
initialValue?: string,
+ validValues?: string[],
): [string | undefined, (nextParamValue: ParamValue) => void] {
- function getCurrentValue(): ParamValue {
+ function getCurrentValue(currentValue?: string): ParamValue {
const path = window.location.hash.slice(1);
if (path) {
- return getStepParam(path);
+ const value = getStepParam(path);
+ return !validValues || validValues.includes(value) ? value : currentValue;
}
+
+ return initialValue;
}
const [value, setValue] = useState(initialValue ?? getCurrentValue);
diff --git a/app/javascript/packs/application.ts b/app/javascript/packs/application.ts
index 6cd8cb725d5..ad2ce264d1c 100644
--- a/app/javascript/packs/application.ts
+++ b/app/javascript/packs/application.ts
@@ -2,8 +2,3 @@ import { accordion, banner, skipnav } from '@18f/identity-design-system';
const components = [accordion, banner, skipnav];
components.forEach((component) => component.on());
-const mainContent = document.getElementById('main-content');
-document.querySelector('.usa-skipnav')?.addEventListener('click', (event) => {
- event.preventDefault();
- mainContent?.scrollIntoView();
-});
From 21d5b412ea73b5006b7cb65738c3c706e40b41b4 Mon Sep 17 00:00:00 2001
From: Amir Reavis-Bey
Date: Thu, 1 Feb 2024 13:47:33 -0500
Subject: [PATCH 05/25] LG-12033: selfie_check_performed verifies the vendor
performed a selfie check (#9983)
* convert DocumentCaputureSessionResult selfie_check_performed from an attribute to a method
* remove selfie_check_performed constructur argument for DocAuth Response
* ResultResponse get results calls
* rename selfie_check_performed method in document session result
* update selfie_check_performed mocks
* remove selfie_check_performed arg
* fix selfie_check_performed method name
* document capture spec to use image with liveness
* analytics selfie spec to mock selfie_status
* hybrid feature spec to use images with liveness data
* remove commented lines
* refactor selfie check performed into selfie concern
* selfie_check_performed must remain for 50/50
* use liveness yaml test file
* update state checked
* liveness success yml to use same pii as mock default
* update spec to use the mock default pii also used in the yml test file
* Internal, Document Authentication, Refactor selfie_check_performed to reflect result from vendor
* changelog: Internal, Document Authentication, Refactor selfie_check_performed to reflect result from vendor
---
.../concerns/idv/document_capture_concern.rb | 4 ++--
app/models/document_capture_session.rb | 1 -
.../lexis_nexis/responses/true_id_response.rb | 1 -
.../doc_auth/mock/doc_auth_mock_client.rb | 5 ++--
app/services/doc_auth/mock/result_response.rb | 10 ++++----
app/services/doc_auth/response.rb | 4 +---
app/services/doc_auth/selfie_concern.rb | 6 +++++
.../document_capture_session_result.rb | 2 ++
.../idv/document_capture_concern_spec.rb | 2 +-
.../idv/image_uploads_controller_spec.rb | 11 +++++----
.../idv/link_sent_controller_spec.rb | 2 +-
spec/features/idv/analytics_spec.rb | 4 ++++
.../idv/doc_auth/document_capture_spec.rb | 3 +--
.../idv/hybrid_mobile/hybrid_mobile_spec.rb | 3 +--
.../ial2_test_portrait_match_success.yml | 23 ++++++++++---------
spec/forms/idv/api_image_upload_form_spec.rb | 16 ++++++-------
.../mock/doc_auth_mock_client_spec.rb | 4 ----
.../doc_auth/mock/result_response_spec.rb | 5 ++--
spec/services/doc_auth_router_spec.rb | 4 ++--
.../features/document_capture_step_helper.rb | 10 ++++++++
20 files changed, 66 insertions(+), 54 deletions(-)
diff --git a/app/controllers/concerns/idv/document_capture_concern.rb b/app/controllers/concerns/idv/document_capture_concern.rb
index 302bc509d58..7c45dc786e9 100644
--- a/app/controllers/concerns/idv/document_capture_concern.rb
+++ b/app/controllers/concerns/idv/document_capture_concern.rb
@@ -36,7 +36,7 @@ def extract_pii_from_doc(user, response, store_in_session: false)
idv_session.had_barcode_read_failure = response.attention_with_barcode?
if store_in_session
idv_session.pii_from_doc = response.pii_from_doc
- idv_session.selfie_check_performed = response.selfie_check_performed
+ idv_session.selfie_check_performed = response.selfie_check_performed?
end
end
@@ -49,7 +49,7 @@ def stored_result
end
def selfie_requirement_met?
- !decorated_sp_session.selfie_required? || stored_result.selfie_check_performed
+ !decorated_sp_session.selfie_required? || stored_result.selfie_check_performed?
end
private
diff --git a/app/models/document_capture_session.rb b/app/models/document_capture_session.rb
index 94ca97fe105..c2a42e5c118 100644
--- a/app/models/document_capture_session.rb
+++ b/app/models/document_capture_session.rb
@@ -17,7 +17,6 @@ def store_result_from_response(doc_auth_response)
session_result.pii = doc_auth_response.pii_from_doc
session_result.captured_at = Time.zone.now
session_result.attention_with_barcode = doc_auth_response.attention_with_barcode?
- session_result.selfie_check_performed = doc_auth_response.selfie_check_performed?
session_result.doc_auth_success = doc_auth_response.doc_auth_success?
session_result.selfie_status = doc_auth_response.selfie_status
EncryptedRedisStructStorage.store(
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 9c3b96748c0..9e326260ed4 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
@@ -54,7 +54,6 @@ def initialize(http_response, config, liveness_checking_enabled = false)
errors: error_messages,
extra: extra_attributes,
pii_from_doc: pii_from_doc,
- selfie_check_performed: liveness_checking_enabled,
)
rescue StandardError => e
NewRelic::Agent.notice_error(e)
diff --git a/app/services/doc_auth/mock/doc_auth_mock_client.rb b/app/services/doc_auth/mock/doc_auth_mock_client.rb
index ee285dd47e0..82427f1bd49 100644
--- a/app/services/doc_auth/mock/doc_auth_mock_client.rb
+++ b/app/services/doc_auth/mock/doc_auth_mock_client.rb
@@ -75,10 +75,10 @@ def post_images(
back_image_response = post_back_image(image: back_image, instance_id: instance_id)
return back_image_response unless back_image_response.success?
- get_results(instance_id: instance_id, selfie_check_performed: liveness_checking_required)
+ get_results(instance_id: instance_id)
end
- def get_results(instance_id:, selfie_check_performed:)
+ def get_results(instance_id:)
return mocked_response_for_method(__method__) if method_mocked?(__method__)
error_response = http_error_response(self.class.last_uploaded_back_image, 'result')
return error_response if error_response
@@ -90,7 +90,6 @@ def get_results(instance_id:, selfie_check_performed:)
ResultResponse.new(
self.class.last_uploaded_back_image,
- selfie_check_performed,
overriden_config,
)
end
diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb
index 3e4202ca213..8beb314726c 100644
--- a/app/services/doc_auth/mock/result_response.rb
+++ b/app/services/doc_auth/mock/result_response.rb
@@ -7,16 +7,14 @@ class ResultResponse < DocAuth::Response
attr_reader :uploaded_file, :config
- def initialize(uploaded_file, selfie_check_performed, config)
+ def initialize(uploaded_file, config)
@uploaded_file = uploaded_file.to_s
- @selfie_check_performed = selfie_check_performed
@config = config
super(
success: success?,
errors: errors,
pii_from_doc: pii_from_doc,
doc_type_supported: id_type_supported?,
- selfie_check_performed: selfie_check_performed,
selfie_live: selfie_live?,
selfie_quality_good: selfie_quality_good?,
extra: {
@@ -63,7 +61,7 @@ def errors
mock_args[:image_metrics] = image_metrics.symbolize_keys if image_metrics.present?
mock_args[:failed] = failed.map!(&:symbolize_keys) unless failed.nil?
mock_args[:passed] = passed.map!(&:symbolize_keys) if passed.present?
- mock_args[:liveness_enabled] = @selfie_check_performed
+ mock_args[:liveness_enabled] = face_match_result ? true : false
mock_args[:classification_info] = classification_info if classification_info.present?
fake_response_info = create_response_info(**mock_args)
ErrorGenerator.new(config).generate_doc_auth_errors(fake_response_info)
@@ -183,7 +181,7 @@ def doc_auth_result_from_success
def all_doc_capture_values_passing?(doc_auth_result, id_type_supported)
doc_auth_result == 'Passed' &&
id_type_supported &&
- (@selfie_check_performed ? selfie_passed? : true)
+ (selfie_check_performed? ? selfie_passed? : true)
end
def selfie_passed?
@@ -238,7 +236,7 @@ def create_response_info(
image_metrics: merged_image_metrics,
liveness_enabled: liveness_enabled,
classification_info: classification_info,
- portrait_match_results: @selfie_check_performed ? portrait_match_results : nil,
+ portrait_match_results: selfie_check_performed? ? portrait_match_results : nil,
}.compact
end
end
diff --git a/app/services/doc_auth/response.rb b/app/services/doc_auth/response.rb
index 9be626d4ce1..5c311934a82 100644
--- a/app/services/doc_auth/response.rb
+++ b/app/services/doc_auth/response.rb
@@ -18,7 +18,6 @@ def initialize(
pii_from_doc: {},
attention_with_barcode: false,
doc_type_supported: true,
- selfie_check_performed: false,
selfie_live: true,
selfie_quality_good: true
)
@@ -29,7 +28,6 @@ def initialize(
@pii_from_doc = pii_from_doc
@attention_with_barcode = attention_with_barcode
@doc_type_supported = doc_type_supported
- @selfie_check_performed = selfie_check_performed
@selfie_live = selfie_live
@selfie_quality_good = selfie_quality_good
end
@@ -96,7 +94,7 @@ def network_error?
end
def selfie_check_performed?
- @selfie_check_performed
+ false
end
def doc_auth_success?
diff --git a/app/services/doc_auth/selfie_concern.rb b/app/services/doc_auth/selfie_concern.rb
index ec8eab06ced..89e562168fd 100644
--- a/app/services/doc_auth/selfie_concern.rb
+++ b/app/services/doc_auth/selfie_concern.rb
@@ -25,8 +25,14 @@ def error_is_poor_quality(error_message)
error_message == ERROR_TEXTS[:poor_quality]
end
+ def selfie_check_performed?
+ SELFIE_PERFORMED_STATUSES.include?(selfie_status)
+ end
+
private
+ SELFIE_PERFORMED_STATUSES = %i[success fail]
+
ERROR_TEXTS = {
success: 'Successful. Liveness: Live',
not_live: 'Liveness: NotLive',
diff --git a/app/services/document_capture_session_result.rb b/app/services/document_capture_session_result.rb
index 5d09e275242..c1bfac2a57c 100644
--- a/app/services/document_capture_session_result.rb
+++ b/app/services/document_capture_session_result.rb
@@ -18,6 +18,8 @@
:captured_at, :selfie_check_performed, :doc_auth_success, :selfie_status,
:selfie_success]
) do
+ include DocAuth::SelfieConcern
+
def self.redis_key_prefix
'dcs:result'
end
diff --git a/spec/controllers/concerns/idv/document_capture_concern_spec.rb b/spec/controllers/concerns/idv/document_capture_concern_spec.rb
index 67fbae4b89c..c77665906c4 100644
--- a/spec/controllers/concerns/idv/document_capture_concern_spec.rb
+++ b/spec/controllers/concerns/idv/document_capture_concern_spec.rb
@@ -23,7 +23,7 @@ def show
allow(decorated_sp_session).to receive(:selfie_required?).and_return(selfie_required)
allow(controller).to receive(:decorated_sp_session).and_return(decorated_sp_session)
stored_result = instance_double(DocumentCaptureSessionResult)
- allow(stored_result).to receive(:selfie_check_performed).and_return(selfie_check_performed)
+ allow(stored_result).to receive(:selfie_check_performed?).and_return(selfie_check_performed)
allow(controller).to receive(:stored_result).and_return(stored_result)
end
diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb
index f0ab25f4dad..d657d0731a4 100644
--- a/spec/controllers/idv/image_uploads_controller_spec.rb
+++ b/spec/controllers/idv/image_uploads_controller_spec.rb
@@ -5,6 +5,7 @@
let(:document_filename_regex) { /^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}\.[a-z]+$/ }
let(:base64_regex) { /^[a-z0-9+\/]+=*$/i }
+ let(:back_image) { DocAuthImageFixtures.document_back_image_multipart }
let(:selfie_img) { nil }
let(:state_id_number) { 'S59397998' }
@@ -20,8 +21,8 @@
{
front: DocAuthImageFixtures.document_front_image_multipart,
front_image_metadata: '{"glare":99.99}',
- back: DocAuthImageFixtures.document_back_image_multipart,
- selfie: (selfie_img unless selfie_img.nil?),
+ back: back_image,
+ selfie: selfie_img,
back_image_metadata: '{"glare":99.99}',
document_capture_session_uuid: document_capture_session.uuid,
flow_path: flow_path,
@@ -351,6 +352,7 @@
# fake up a response and verify that selfie_check_performed flows through?
context 'selfie included' do
+ let(:back_image) { DocAuthImageFixtures.portrait_match_success_yaml }
let(:selfie_img) { DocAuthImageFixtures.selfie_image_multipart }
before do
@@ -374,7 +376,7 @@
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.selfie_check_performed).to eq(true)
+ expect(document_capture_session.reload.load_result.selfie_check_performed?).to eq(true)
end
end
@@ -1248,6 +1250,7 @@
and_return(double('decorated_session', { selfie_required?: true }))
end
+ let(:back_image) { DocAuthImageFixtures.portrait_match_success_yaml }
let(:selfie_img) { DocAuthImageFixtures.selfie_image_multipart }
it 'returns a successful response' do
@@ -1255,7 +1258,7 @@
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.selfie_check_performed).to eq(true)
+ expect(document_capture_session.reload.load_result.selfie_check_performed?).to eq(true)
end
it 'sends a selfie' do
diff --git a/spec/controllers/idv/link_sent_controller_spec.rb b/spec/controllers/idv/link_sent_controller_spec.rb
index d84960dd502..475f95cb4c6 100644
--- a/spec/controllers/idv/link_sent_controller_spec.rb
+++ b/spec/controllers/idv/link_sent_controller_spec.rb
@@ -145,7 +145,7 @@
allow(load_result).to receive(:attention_with_barcode?).and_return(false)
allow(load_result).to receive(:success?).and_return(load_result_success)
- allow(load_result).to receive(:selfie_check_performed).and_return(false)
+ allow(load_result).to receive(:selfie_check_performed?).and_return(false)
document_capture_session = DocumentCaptureSession.create!(
user: user,
diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb
index 87209d678c5..d0b647e8e50 100644
--- a/spec/features/idv/analytics_spec.rb
+++ b/spec/features/idv/analytics_spec.rb
@@ -850,6 +850,10 @@ def wait_for_event(event, wait)
to receive(:biometric_comparison_required?).
and_return({ biometric_comparison_required: true })
+ allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:success)
+ allow_any_instance_of(DocumentCaptureSessionResult).
+ to receive(:selfie_status).and_return(:success)
+
mobile_device = Browser.new(mobile_user_agent)
allow(BrowserCache).to receive(:parse).and_return(mobile_device)
diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb
index a84172e862d..deccef71931 100644
--- a/spec/features/idv/doc_auth/document_capture_spec.rb
+++ b/spec/features/idv/doc_auth/document_capture_spec.rb
@@ -234,8 +234,7 @@
expect_doc_capture_page_header(t('doc_auth.headings.document_capture_with_selfie'))
expect_doc_capture_id_subheader
expect_doc_capture_selfie_subheader
- attach_images
- attach_selfie
+ attach_liveness_images
submit_images
expect(page).to have_current_path(idv_ssn_url)
diff --git a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb
index fb39426ece8..d1ae5b74861 100644
--- a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb
+++ b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb
@@ -356,8 +356,7 @@
expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url)
- attach_images
- attach_selfie
+ attach_liveness_images
submit_images
expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
diff --git a/spec/fixtures/ial2_test_portrait_match_success.yml b/spec/fixtures/ial2_test_portrait_match_success.yml
index 70b2ec5af5d..6641f442865 100644
--- a/spec/fixtures/ial2_test_portrait_match_success.yml
+++ b/spec/fixtures/ial2_test_portrait_match_success.yml
@@ -1,16 +1,17 @@
document:
- first_name: 'John'
- last_name: 'Doe'
- address1: 1800 F Street
- address2: Apt 3
- city: Bayside
- state: NY
- zipcode: '11364'
- dob: 10/06/1938
- phone: +1 314-555-1212
- state_id_jurisdiction: 'ND'
+ address1: '1 FAKE RD'
+ city: 'GREAT FALLS'
+ dob: '1938-10-06'
+ first_name: 'FAKEY'
+ last_name: 'MCFAKERSON'
+ state: 'MT'
+ state_id_expiration: '2099-12-31'
+ state_id_issued: '2019-12-31'
+ state_id_jurisdiction: ND
state_id_number: '1111111111111'
+ state_id_type: 'drivers_license'
+ zipcode: '59010'
doc_auth_result: Passed
failed_alert: []
portrait_match_results:
- FaceMatchResult: Pass
\ No newline at end of file
+ FaceMatchResult: Pass
diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb
index 9e936aab656..a092af9fdae 100644
--- a/spec/forms/idv/api_image_upload_form_spec.rb
+++ b/spec/forms/idv/api_image_upload_form_spec.rb
@@ -202,17 +202,17 @@
it 'logs analytics' do
expect(irs_attempts_api_tracker).to receive(:idv_document_upload_submitted).with(
{
- address: '1800 F Street',
+ address: '1 FAKE RD',
date_of_birth: '1938-10-06',
document_back_image_filename: nil,
- document_expiration: nil,
+ document_expiration: '2099-12-31',
document_front_image_filename: nil,
document_image_encryption_key: nil,
- document_issued: nil,
+ document_issued: '2019-12-31',
document_number: '1111111111111',
- document_state: 'NY',
- first_name: 'John',
- last_name: 'Doe',
+ document_state: 'MT',
+ first_name: 'FAKEY',
+ last_name: 'MCFAKERSON',
success: true,
},
)
@@ -269,8 +269,8 @@
product_status: nil,
reference: nil,
remaining_attempts: 3,
- state: 'NY',
- state_id_type: nil,
+ state: 'MT',
+ state_id_type: 'drivers_license',
success: true,
user_id: document_capture_session.user.uuid,
vendor_request_time_in_ms: a_kind_of(Float),
diff --git a/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb b/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb
index a22023c982a..0357e5d389a 100644
--- a/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb
+++ b/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb
@@ -27,7 +27,6 @@
)
get_results_response = client.get_results(
instance_id: instance_id,
- selfie_check_performed: liveness_checking_required,
)
expect(create_document_response.success?).to eq(true)
@@ -89,7 +88,6 @@
)
get_results_response = client.get_results(
instance_id: create_document_response.instance_id,
- selfie_check_performed: liveness_checking_required,
)
expect(get_results_response.pii_from_doc).to eq(
@@ -130,7 +128,6 @@
)
get_results_response = client.get_results(
instance_id: create_document_response.instance_id,
- selfie_check_performed: liveness_checking_required,
)
expect(get_results_response.attention_with_barcode?).to eq(false)
errors = get_results_response.errors
@@ -257,7 +254,6 @@
)
response = client.get_results(
instance_id: nil,
- selfie_check_performed: liveness_checking_required,
)
expect(response).to be_a(DocAuth::Response)
expect(response.success?).to eq(false)
diff --git a/spec/services/doc_auth/mock/result_response_spec.rb b/spec/services/doc_auth/mock/result_response_spec.rb
index 06b40ddb8c4..10b2ca04f75 100644
--- a/spec/services/doc_auth/mock/result_response_spec.rb
+++ b/spec/services/doc_auth/mock/result_response_spec.rb
@@ -2,7 +2,6 @@
RSpec.describe DocAuth::Mock::ResultResponse do
let(:warn_notifier) { instance_double('Proc') }
- let(:selfie_check_performed) { false }
subject(:response) do
config = DocAuth::Mock::Config.new(
@@ -11,7 +10,7 @@
glare_threshold: 40,
warn_notifier: warn_notifier,
)
- described_class.new(input, selfie_check_performed, config)
+ described_class.new(input, config)
end
context 'with an image file' do
@@ -255,7 +254,7 @@
glare_threshold: 40,
},
)
- described_class.new(input, selfie_check_performed, config)
+ described_class.new(input, config)
end
let(:input) do
diff --git a/spec/services/doc_auth_router_spec.rb b/spec/services/doc_auth_router_spec.rb
index c0098bf8873..bdf4214871f 100644
--- a/spec/services/doc_auth_router_spec.rb
+++ b/spec/services/doc_auth_router_spec.rb
@@ -185,7 +185,7 @@ def reload_ab_test_initializer!
)
response = I18n.with_locale(:es) do
- proxy.get_results(instance_id: 'abcdef', selfie_check_performed: false)
+ proxy.get_results(instance_id: 'abcdef')
end
expect(response.errors[:some_other_key]).to eq(['will not be translated'])
@@ -208,7 +208,7 @@ def reload_ab_test_initializer!
),
)
- response = proxy.get_results(instance_id: 'abcdef', selfie_check_performed: false)
+ response = proxy.get_results(instance_id: 'abcdef')
expect(response.errors[:network]).to eq(I18n.t('doc_auth.errors.general.network_error'))
end
diff --git a/spec/support/features/document_capture_step_helper.rb b/spec/support/features/document_capture_step_helper.rb
index f3e7aa43861..c5ed54a7438 100644
--- a/spec/support/features/document_capture_step_helper.rb
+++ b/spec/support/features/document_capture_step_helper.rb
@@ -16,6 +16,16 @@ def attach_images(file = Rails.root.join('app', 'assets', 'images', 'logo.png'))
attach_file t('doc_auth.headings.document_capture_back'), file, make_visible: true
end
+ def attach_liveness_images(
+ file = Rails.root.join(
+ 'spec', 'fixtures',
+ 'ial2_test_portrait_match_success.yml'
+ )
+ )
+ attach_images(file)
+ attach_selfie
+ end
+
def attach_selfie(file = Rails.root.join('app', 'assets', 'images', 'logo.png'))
attach_file t('doc_auth.headings.document_capture_selfie'), file, make_visible: true
end
From 913b82bae820c3dda27ab29efd89d97e1d911c15 Mon Sep 17 00:00:00 2001
From: Andrew Duthie <1779930+aduth@users.noreply.github.com>
Date: Thu, 1 Feb 2024 16:49:02 -0500
Subject: [PATCH 06/25] Send RISC password reset to confirmed emails (#10022)
changelog: Internal, RISC, Send RISC password reset to confirmed emails
---
app/services/reset_user_password.rb | 2 +-
spec/services/reset_user_password_spec.rb | 5 +++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/app/services/reset_user_password.rb b/app/services/reset_user_password.rb
index d4152b4836c..400df73db3e 100644
--- a/app/services/reset_user_password.rb
+++ b/app/services/reset_user_password.rb
@@ -32,7 +32,7 @@ def log_event
end
def notify_user
- user.email_addresses.each do |email_address|
+ user.confirmed_email_addresses.each do |email_address|
UserMailer.with(user: user, email_address: email_address).please_reset_password.
deliver_now_or_later
end
diff --git a/spec/services/reset_user_password_spec.rb b/spec/services/reset_user_password_spec.rb
index 0b07529d534..f7aafe5f929 100644
--- a/spec/services/reset_user_password_spec.rb
+++ b/spec/services/reset_user_password_spec.rb
@@ -19,12 +19,13 @@
to(change { user.events.password_invalidated.size }.from(0).to(1))
end
- it 'notifies the user via email to each of their email addresses' do
+ it 'notifies the user via email to each of their confirmed email addresses' do
+ create(:email_address, user:, email: Faker::Internet.safe_email, confirmed_at: nil)
expect { call }.
to(change { ActionMailer::Base.deliveries.count }.by(2))
mails = ActionMailer::Base.deliveries.last(2)
- expect(mails.map(&:to).flatten).to match_array(user.email_addresses.map(&:email))
+ expect(mails.map(&:to).flatten).to match_array(user.confirmed_email_addresses.map(&:email))
end
it 'clears all remembered browsers by updating the remember_device_revoked_at timestamp' do
From abeed61048d1a2a77fe8f8ff78915190df44e685 Mon Sep 17 00:00:00 2001
From: Andrew Duthie <1779930+aduth@users.noreply.github.com>
Date: Fri, 2 Feb 2024 10:38:17 -0500
Subject: [PATCH 07/25] Load error tracking script asynchronously (#10013)
* Load error tracking script asynchronously
changelog: Internal, Performance, Improve performance of JavaScript loading for error tracking
* Generalize attributes in favor of named keywords
---
app/helpers/script_helper.rb | 43 +++++++++-----------
app/views/layouts/base.html.erb | 4 +-
app/views/layouts/component_preview.html.erb | 2 +-
spec/helpers/script_helper_spec.rb | 29 +++++++++++--
4 files changed, 48 insertions(+), 30 deletions(-)
diff --git a/app/helpers/script_helper.rb b/app/helpers/script_helper.rb
index 5600061a3eb..1c9ce9925d0 100644
--- a/app/helpers/script_helper.rb
+++ b/app/helpers/script_helper.rb
@@ -6,33 +6,28 @@ def javascript_include_tag_without_preload(...)
without_preload_links_header { javascript_include_tag(...) }
end
- def javascript_packs_tag_once(*names, prepend: false)
- @scripts ||= []
- if prepend
- @scripts = names | @scripts
- else
- @scripts |= names
- end
+ def javascript_packs_tag_once(*names, **attributes)
+ @scripts = @scripts.to_h.merge(names.index_with(attributes))
nil
end
alias_method :enqueue_component_scripts, :javascript_packs_tag_once
- def render_javascript_pack_once_tags(*names)
- names = names.presence || @scripts
- if names && (sources = AssetSources.get_sources(*names)).present?
- safe_join(
- [
- javascript_assets_tag(*names),
- *sources.map do |source|
- javascript_include_tag(
- source,
- crossorigin: local_crossorigin_sources? ? true : nil,
- integrity: AssetSources.get_integrity(source),
- )
- end,
- ],
- )
+ def render_javascript_pack_once_tags(...)
+ capture do
+ javascript_packs_tag_once(...)
+ return if @scripts.blank?
+ concat javascript_assets_tag
+ @scripts.each do |name, attributes|
+ AssetSources.get_sources(name).each do |source|
+ concat javascript_include_tag(
+ source,
+ **attributes,
+ crossorigin: local_crossorigin_sources? ? true : nil,
+ integrity: AssetSources.get_integrity(source),
+ )
+ end
+ end
end
end
@@ -46,8 +41,8 @@ def local_crossorigin_sources?
Rails.env.development? && ENV['WEBPACK_PORT'].present?
end
- def javascript_assets_tag(*names)
- assets = AssetSources.get_assets(*names)
+ def javascript_assets_tag
+ assets = AssetSources.get_assets(*@scripts.keys)
if assets.present?
asset_map = assets.index_with { |path| asset_path(path, host: asset_host(path)) }
content_tag(
diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb
index 571d6cc8ef5..60422ebd37b 100644
--- a/app/views/layouts/base.html.erb
+++ b/app/views/layouts/base.html.erb
@@ -106,8 +106,8 @@
{ type: 'application/json', data: { config: '' } },
false,
) %>
- <%= javascript_packs_tag_once('application', prepend: true) %>
- <%= javascript_packs_tag_once('track-errors') if BrowserSupport.supported?(request.user_agent) %>
+ <%= javascript_packs_tag_once('application') %>
+ <%= javascript_packs_tag_once('track-errors', async: true) if BrowserSupport.supported?(request.user_agent) %>
<%= render_javascript_pack_once_tags %>
<%= render 'shared/dap_analytics' if IdentityConfig.store.participate_in_dap && !session_with_trust? %>
diff --git a/app/views/layouts/component_preview.html.erb b/app/views/layouts/component_preview.html.erb
index 2e70f8fea54..5a73c75a763 100644
--- a/app/views/layouts/component_preview.html.erb
+++ b/app/views/layouts/component_preview.html.erb
@@ -16,7 +16,7 @@
<% else %>
<%= yield %>
<% end %>
- <%= javascript_packs_tag_once('application', prepend: true) %>
+ <%= javascript_packs_tag_once('application') %>
<%= render_javascript_pack_once_tags %>