, nil] errors error response from api call
+ # @param [String, nil] exception Error exception from api call
+ # @param [Boolean] timed_out set whether api call timed out
+ # @param [String] review_status TMX decision on the user
+ # @param [String] account_lex_id LexID associated with the response.
+ # @param [String] session_id Session ID associated with response
+ # @param [Hash] response_body total response body for api call
+ # Result when threatmetrix is completed for account creation and result
+ def account_creation_tmx_result(
+ client:,
+ success:,
+ errors:,
+ exception:,
+ timed_out:,
+ transaction_id:,
+ review_status:,
+ account_lex_id:,
+ session_id:,
+ response_body:,
+ **extra
+ )
+ track_event(
+ :account_creation_tmx_result,
+ client:,
+ success:,
+ errors:,
+ exception:,
+ timed_out:,
+ transaction_id:,
+ review_status:,
+ account_lex_id:,
+ session_id:,
+ response_body:,
+ **extra,
+ )
+ end
+
# @param [Boolean] success
# When a user submits a form to delete their account
def account_delete_submitted(success:, **extra)
diff --git a/app/services/doc_auth/socure/request.rb b/app/services/doc_auth/socure/request.rb
index 64fa97efd05..cb7644c14a3 100644
--- a/app/services/doc_auth/socure/request.rb
+++ b/app/services/doc_auth/socure/request.rb
@@ -6,9 +6,11 @@ class Request
def fetch
# return DocAuth::Response with DocAuth::Error if workflow is invalid
http_response = send_http_request
- return handle_invalid_response(http_response) unless http_response.success?
-
- handle_http_response(http_response)
+ if http_response&.success? && http_response.body.present?
+ handle_http_response(http_response)
+ else
+ handle_invalid_response(http_response)
+ end
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError => e
handle_connection_error(exception: e)
end
@@ -33,16 +35,27 @@ def handle_http_response(_response)
end
def handle_invalid_response(http_response)
- begin
- if http_response.body.present?
- warn(http_response.body)
- JSON.parse(http_response.body)
- else
- {}
- end
+ message = [
+ self.class.name,
+ 'Unexpected HTTP response',
+ http_response&.status,
+ ].join(' ')
+ exception = DocAuth::RequestError.new(message, http_response&.status)
+
+ response_body = begin
+ http_response&.body.present? ? JSON.parse(http_response.body) : {}
rescue JSON::JSONError
{}
end
+ handle_connection_error(
+ exception: exception,
+ status: response_body.dig('status'),
+ status_message: response_body.dig('msg'),
+ )
+ end
+
+ def handle_connection_error(exception:, status: nil, status_message: nil)
+ raise NotImplementedError
end
def send_http_get_request
diff --git a/app/services/doc_auth/socure/requests/document_request.rb b/app/services/doc_auth/socure/requests/document_request.rb
index 796b6cb22b2..4a6f5b0920f 100644
--- a/app/services/doc_auth/socure/requests/document_request.rb
+++ b/app/services/doc_auth/socure/requests/document_request.rb
@@ -27,7 +27,7 @@ def lang(language)
def body
redirect = {
- method: 'POST',
+ method: 'GET',
url: redirect_url,
}
@@ -47,6 +47,20 @@ def handle_http_response(http_response)
JSON.parse(http_response.body, symbolize_names: true)
end
+ def handle_connection_error(exception:, status: nil, status_message: nil)
+ NewRelic::Agent.notice_error(exception)
+ {
+ success: false,
+ errors: { network: true },
+ exception: exception,
+ extra: {
+ vendor: 'Socure',
+ vendor_status: status,
+ vendor_status_message: status_message,
+ }.compact,
+ }
+ end
+
def method
:post
end
diff --git a/app/services/doc_auth/socure/requests/docv_result_request.rb b/app/services/doc_auth/socure/requests/docv_result_request.rb
index 6d53cc39d7a..7f4bc26d116 100644
--- a/app/services/doc_auth/socure/requests/docv_result_request.rb
+++ b/app/services/doc_auth/socure/requests/docv_result_request.rb
@@ -27,6 +27,20 @@ def handle_http_response(http_response)
)
end
+ def handle_connection_error(exception:, status: nil, status_message: nil)
+ NewRelic::Agent.notice_error(exception)
+ DocAuth::Response.new(
+ success: false,
+ errors: { network: true },
+ exception: exception,
+ extra: {
+ vendor: 'Socure',
+ vendor_status: status,
+ vendor_status_message: status_message,
+ }.compact,
+ )
+ end
+
def document_capture_session
@document_capture_session ||=
DocumentCaptureSession.find_by!(uuid: document_capture_session_uuid)
diff --git a/app/services/doc_auth/socure/responses/docv_result_response.rb b/app/services/doc_auth/socure/responses/docv_result_response.rb
index 23b17f88e9a..aaf4413da27 100644
--- a/app/services/doc_auth/socure/responses/docv_result_response.rb
+++ b/app/services/doc_auth/socure/responses/docv_result_response.rb
@@ -32,7 +32,7 @@ class DocvResultResponse < DocAuth::Response
expiration_date: %w[documentVerification documentData expirationDate],
}.freeze
- def initialize(http_response: nil, biometric_comparison_required: false)
+ def initialize(http_response:, biometric_comparison_required: false)
@http_response = http_response
@biometric_comparison_required = biometric_comparison_required
@pii_from_doc = read_pii
@@ -110,17 +110,23 @@ def get_data(path)
end
def parsed_response_body
- @parsed_response_body ||= JSON.parse(http_response.body).with_indifferent_access
+ @parsed_response_body ||= begin
+ http_response&.body.present? ? JSON.parse(
+ http_response.body,
+ ).with_indifferent_access : {}
+ rescue JSON::JSONError
+ {}
+ end
end
def state_id_type
type = get_data(DATA_PATHS[:id_type])
- type.gsub(/\W/, '').underscore
+ type&.gsub(/\W/, '')&.underscore
end
def parse_date(date_string)
Date.parse(date_string)
- rescue ArgumentError
+ rescue ArgumentError, TypeError
message = {
event: 'Failure to parse Socure ID+ date',
}.to_json
diff --git a/app/services/form_response.rb b/app/services/form_response.rb
index e62f67c9613..3fdbbe93384 100644
--- a/app/services/form_response.rb
+++ b/app/services/form_response.rb
@@ -25,6 +25,8 @@ def to_h
hash
end
+ alias_method :to_hash, :to_h
+
def merge(other)
self.class.new(
success: success? && other.success?,
diff --git a/app/services/funnel/registration/add_mfa.rb b/app/services/funnel/registration/add_mfa.rb
index 14ac8595e3c..99ef121b994 100644
--- a/app/services/funnel/registration/add_mfa.rb
+++ b/app/services/funnel/registration/add_mfa.rb
@@ -3,14 +3,22 @@
module Funnel
module Registration
class AddMfa
- def self.call(user_id, mfa_method, analytics)
+ def self.call(user_id, mfa_method, analytics, threatmetrix_attrs)
now = Time.zone.now
funnel = RegistrationLog.create_or_find_by(user_id: user_id)
return if funnel.registered_at.present?
analytics.user_registration_user_fully_registered(mfa_method: mfa_method)
+ process_threatmetrix_for_user(
+ threatmetrix_attrs,
+ )
funnel.update!(registered_at: now)
end
+
+ def self.process_threatmetrix_for_user(threatmetrix_attrs)
+ return unless FeatureManagement.account_creation_device_profiling_collecting_enabled?
+ AccountCreationThreatMetrixJob.perform_later(**threatmetrix_attrs)
+ end
end
end
end
diff --git a/app/services/proofing/lexis_nexis/config.rb b/app/services/proofing/lexis_nexis/config.rb
index e650a58053b..084cd705178 100644
--- a/app/services/proofing/lexis_nexis/config.rb
+++ b/app/services/proofing/lexis_nexis/config.rb
@@ -15,6 +15,7 @@ module LexisNexis
:request_timeout,
:org_id,
:api_key,
+ :ddp_policy,
keyword_init: true,
allowed_members: [
:instant_verify_workflow,
diff --git a/app/services/proofing/lexis_nexis/ddp/verification_request.rb b/app/services/proofing/lexis_nexis/ddp/verification_request.rb
index a918500fb93..85455809896 100644
--- a/app/services/proofing/lexis_nexis/ddp/verification_request.rb
+++ b/app/services/proofing/lexis_nexis/ddp/verification_request.rb
@@ -10,29 +10,29 @@ def build_request_body
{
api_key: config.api_key,
org_id: config.org_id,
- account_address_street1: applicant[:address1],
+ account_address_street1: applicant[:address1] || '',
account_address_street2: applicant[:address2] || '',
- account_address_city: applicant[:city],
- account_address_state: applicant[:state],
- account_address_country: 'US',
- account_address_zip: applicant[:zipcode],
+ account_address_city: applicant[:city] || '',
+ account_address_state: applicant[:state] || '',
+ account_address_country: applicant[:state] ? 'US' : '',
+ account_address_zip: applicant[:zipcode] || '',
account_date_of_birth: applicant[:dob] ?
Date.parse(applicant[:dob]).strftime('%Y%m%d') : '',
account_email: applicant[:email],
- account_first_name: applicant[:first_name],
- account_last_name: applicant[:last_name],
+ account_first_name: applicant[:first_name] || '',
+ account_last_name: applicant[:last_name] || '',
account_telephone: '', # applicant[:phone], decision was made not to send phone
- account_drivers_license_number: applicant[:state_id_number]&.gsub(/\W/, ''),
- account_drivers_license_type: 'us_dl',
- account_drivers_license_issuer: applicant[:state_id_jurisdiction].to_s.strip,
+ account_drivers_license_number: applicant[:state_id_number]&.gsub(/\W/, '') || '',
+ account_drivers_license_type: applicant[:state_id_number] ? 'us_dl' : '',
+ account_drivers_license_issuer: applicant[:state_id_jurisdiction].to_s.strip || '',
event_type: 'ACCOUNT_CREATION',
- policy: IdentityConfig.store.lexisnexis_threatmetrix_policy,
+ policy: config.ddp_policy,
service_type: 'all',
session_id: applicant[:threatmetrix_session_id],
- national_id_number: applicant[:ssn].gsub(/\D/, ''),
- national_id_type: 'US_SSN',
+ national_id_number: applicant[:ssn]&.gsub(/\D/, '') || '',
+ national_id_type: applicant[:ssn] ? 'US_SSN' : '',
input_ip_address: applicant[:request_ip],
- local_attrib_1: applicant[:uuid_prefix],
+ local_attrib_1: applicant[:uuid_prefix] || '',
}.to_json
end
diff --git a/app/services/proofing/resolution/plugins/threat_metrix_plugin.rb b/app/services/proofing/resolution/plugins/threat_metrix_plugin.rb
index 03fa63dbeab..f16176b9911 100644
--- a/app/services/proofing/resolution/plugins/threat_metrix_plugin.rb
+++ b/app/services/proofing/resolution/plugins/threat_metrix_plugin.rb
@@ -46,6 +46,7 @@ def proofer
api_key: IdentityConfig.store.lexisnexis_threatmetrix_api_key,
org_id: IdentityConfig.store.lexisnexis_threatmetrix_org_id,
base_url: IdentityConfig.store.lexisnexis_threatmetrix_base_url,
+ ddp_policy: IdentityConfig.store.lexisnexis_threatmetrix_policy,
)
end
end
diff --git a/app/services/send_add_email_confirmation.rb b/app/services/send_add_email_confirmation.rb
index fbeb8d900a8..3fb0641b627 100644
--- a/app/services/send_add_email_confirmation.rb
+++ b/app/services/send_add_email_confirmation.rb
@@ -7,9 +7,10 @@ def initialize(user)
@user = user
end
- def call(email_address, in_select_email_flow = nil)
+ def call(email_address:, in_select_email_flow: nil, request_id: nil)
@email_address = email_address
@in_select_email_flow = in_select_email_flow
+ @request_id = request_id
update_email_address_record
send_email
end
@@ -24,7 +25,7 @@ def confirmation_sent_at
email_address.confirmation_sent_at
end
- attr_reader :email_address, :in_select_email_flow
+ attr_reader :email_address, :in_select_email_flow, :request_id
def update_email_address_record
email_address.update!(
@@ -59,8 +60,9 @@ def send_email_associated_with_another_account_email
def send_confirmation_email
UserMailer.with(user: user, email_address: email_address).add_email(
- confirmation_token,
- in_select_email_flow,
+ token: confirmation_token,
+ from_select_email_flow: in_select_email_flow,
+ request_id:,
).deliver_now_or_later
end
end
diff --git a/app/views/accounts/connected_accounts/selected_email/edit.html.erb b/app/views/accounts/connected_accounts/selected_email/edit.html.erb
index f2a57f126e5..3347832195c 100644
--- a/app/views/accounts/connected_accounts/selected_email/edit.html.erb
+++ b/app/views/accounts/connected_accounts/selected_email/edit.html.erb
@@ -4,7 +4,7 @@
<% c.with_header(id: 'select-email-heading') { t('titles.select_email') } %>
- <%= t('help_text.select_preferred_email', sp: @identity.display_name, app_name: APP_NAME) %>
+ <%= t('help_text.select_preferred_email_html', sp: @identity.display_name) %>
<%= simple_form_for(
@@ -30,7 +30,7 @@
]
end,
) %>
- <%= f.submit(t('help_text.requested_attributes.change_email_link'), class: 'margin-top-1') %>
+ <%= f.submit(t('help_text.requested_attributes.select_email_link'), class: 'margin-top-1') %>
<% end %>
<%= render ButtonComponent.new(
diff --git a/app/views/sign_up/registrations/new.html.erb b/app/views/sign_up/registrations/new.html.erb
index c31939994e6..6699b21dac1 100644
--- a/app/views/sign_up/registrations/new.html.erb
+++ b/app/views/sign_up/registrations/new.html.erb
@@ -17,15 +17,6 @@
<%= render PageHeadingComponent.new.with_content(t('headings.create_account_new_users')) %>
-<% if FeatureManagement.account_creation_device_profiling_collecting_enabled? %>
- <%= render partial: 'shared/threat_metrix_profiling',
- locals: {
- threatmetrix_session_id:,
- threatmetrix_javascript_urls:,
- threatmetrix_iframe_url:,
- } %>
-<% end %>
-
<%= simple_form_for(@register_user_email_form, url: sign_up_register_path) do |f| %>
<%= render ValidatedFieldComponent.new(
form: f,
@@ -51,6 +42,15 @@
required: true,
) %>
+ <% if FeatureManagement.account_creation_device_profiling_collecting_enabled? %>
+ <%= render partial: 'shared/threat_metrix_profiling',
+ locals: {
+ threatmetrix_session_id:,
+ threatmetrix_javascript_urls:,
+ threatmetrix_iframe_url:,
+ } %>
+ <% end %>
+
<%= f.submit t('forms.buttons.submit.default'), class: 'display-block margin-y-5' %>
<% end %>
diff --git a/app/views/sign_up/select_email/show.html.erb b/app/views/sign_up/select_email/show.html.erb
index 53b0c04e5de..00a5134e774 100644
--- a/app/views/sign_up/select_email/show.html.erb
+++ b/app/views/sign_up/select_email/show.html.erb
@@ -4,7 +4,7 @@
<% c.with_header(id: 'select-email-heading') { t('titles.select_email') } %>
- <%= I18n.t('help_text.select_preferred_email', sp: @sp_name, app_name: APP_NAME) %>
+ <%= I18n.t('help_text.select_preferred_email_html', sp: @sp_name) %>
<%= simple_form_for(@select_email_form, url: sign_up_select_email_path) do |f| %>
@@ -26,7 +26,7 @@
]
end,
) %>
- <%= f.submit t('help_text.requested_attributes.change_email_link'), class: 'margin-top-1' %>
+ <%= f.submit t('help_text.requested_attributes.select_email_link'), class: 'margin-top-1' %>
<% end %>
<%= render ButtonComponent.new(
diff --git a/config/application.yml.default b/config/application.yml.default
index 8a39f3775fd..74b313ec0a3 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -207,6 +207,7 @@ lexisnexis_request_mode: testing
###################################################################
# LexisNexis DDP/ThreatMetrix #####################################
lexisnexis_threatmetrix_api_key:
+lexisnexis_threatmetrix_authentication_policy: '1234'
lexisnexis_threatmetrix_base_url:
lexisnexis_threatmetrix_js_signing_cert: ''
lexisnexis_threatmetrix_mock_enabled: true
diff --git a/config/locales/en.yml b/config/locales/en.yml
index c9377403782..c0a8740407b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -976,11 +976,12 @@ help_text.requested_attributes.full_name: Full name
help_text.requested_attributes.ial2_reverified_consent_info_html: 'Because you verified your identity again, we need your permission to share this information with %{sp_html}:'
help_text.requested_attributes.intro_html: 'We’ll share this information with %{sp_html}:'
help_text.requested_attributes.phone: Phone number
+help_text.requested_attributes.select_email_link: Select email
help_text.requested_attributes.social_security_number: Social Security number
help_text.requested_attributes.verified_at: Updated on
help_text.requested_attributes.x509_issuer: PIV/CAC Issuer
help_text.requested_attributes.x509_subject: PIV/CAC Identity
-help_text.select_preferred_email: You may change which email you share with %{sp} since you have multiple emails associated with your %{app_name} account.
+help_text.select_preferred_email_html: Select or add the email you’d like to use to access %{sp}.
i18n.language: Language
i18n.locale.en: English
i18n.locale.es: Español
@@ -1690,7 +1691,7 @@ two_factor_authentication.max_generic_login_attempts_reached: For your security,
two_factor_authentication.max_otp_login_attempts_reached: For your security, your account is temporarily locked because you have entered the one-time code incorrectly too many times.
two_factor_authentication.max_otp_requests_reached: For your security, your account is temporarily locked because you have requested a one-time code too many times.
two_factor_authentication.max_personal_key_login_attempts_reached: For your security, your account is temporarily locked because you have entered the personal key incorrectly too many times.
-two_factor_authentication.max_piv_cac_login_attempts_reached: For your security, your account is temporarily locked because you have presented your piv/cac credential incorrectly too many times.
+two_factor_authentication.max_piv_cac_login_attempts_reached: For your security, your account is temporarily locked because you have presented your PIV/CAC credential incorrectly too many times.
two_factor_authentication.mobile_terms_of_service: Mobile terms of service
two_factor_authentication.opt_in.error_retry: Sorry, we are having trouble opting you in. Please try again.
two_factor_authentication.opt_in.opted_out_html: You’ve opted out of receiving text messages at %{phone_number_html}. You can opt in and receive a security code again to that phone number.
diff --git a/config/locales/es.yml b/config/locales/es.yml
index f27cbad7794..ac4c70204ad 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -987,11 +987,12 @@ help_text.requested_attributes.full_name: Nombre completo
help_text.requested_attributes.ial2_reverified_consent_info_html: 'Como volvió a verificar su identidad, necesitamos su permiso para divulgar esta información a %{sp_html}:'
help_text.requested_attributes.intro_html: 'Divulgaremos esta información a %{sp_html}:'
help_text.requested_attributes.phone: Número de teléfono
+help_text.requested_attributes.select_email_link: Seleccionar correo electrónico
help_text.requested_attributes.social_security_number: Número de Seguro Social
help_text.requested_attributes.verified_at: Actualizado en
help_text.requested_attributes.x509_issuer: Emisor de la tarjeta PIV o CAC
help_text.requested_attributes.x509_subject: Identidad de la tarjeta PIV o CAC
-help_text.select_preferred_email: Puede cambiar el correo electrónico que comparte con %{sp} ya que tiene varios correos electrónicos asociados a su cuenta de %{app_name}.
+help_text.select_preferred_email_html: Seleccione o agregue el correo electrónico que desea utilizar para acceder a %{sp}.
i18n.language: Idioma
i18n.locale.en: English
i18n.locale.es: Español
@@ -1206,7 +1207,7 @@ image_description.warning: Señal amarilla de precaución
in_person_proofing.body.barcode.cancel_link_text: Cancele su código de barras
in_person_proofing.body.barcode.close_window: Ya puede cerrar esta ventana.
in_person_proofing.body.barcode.deadline: Debe acudir a cualquier oficina de correos participante antes del %{deadline}
-in_person_proofing.body.barcode.deadline_restart: Si vence el plazo, su información no se guardará y tendrá que reiniciar el proceso.
+in_person_proofing.body.barcode.deadline_restart: Si acude después del plazo, su información no se guardará y tendrá que reiniciar el proceso.
in_person_proofing.body.barcode.eipp_tag: Código de barras piloto mejorado GSA
in_person_proofing.body.barcode.eipp_what_to_bring: 'Según el tipo de identificación que tenga, es posible que deba presentar documentos comprobatorios. Lea con atención las opciones siguientes:'
in_person_proofing.body.barcode.email_sent: Enviamos el código de barras y la información más abajo al correo electrónico que usó para iniciar sesión
@@ -1569,7 +1570,7 @@ step_indicator.status.complete: Completado
step_indicator.status.current: Este paso
step_indicator.status.not_complete: No completado
time.am: a. m.
-time.formats.event_date: '%B %-d, %Y'
+time.formats.event_date: '%-d de %B de %Y'
time.formats.event_time: '%-l:%M %p'
time.formats.event_timestamp: '%B %-d, %Y a las %-l:%M %p'
time.formats.event_timestamp_js: '%{month} %{day}, %{year} a las %{hour}:%{minute} %{day_period}'
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 2a41643f85c..f419b9ae3d0 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -976,11 +976,12 @@ help_text.requested_attributes.full_name: Nom complet
help_text.requested_attributes.ial2_reverified_consent_info_html: 'Étant donné que vous avez revérifié votre identité, nous avons besoin de votre autorisation pour partager ces informations avec %{sp_html} :'
help_text.requested_attributes.intro_html: 'Nous partagerons ces informations avec %{sp_html}:'
help_text.requested_attributes.phone: Numéro de téléphone
+help_text.requested_attributes.select_email_link: Sélectionner l’e-mail
help_text.requested_attributes.social_security_number: Numéro de sécurité sociale
help_text.requested_attributes.verified_at: Mis à jour le
help_text.requested_attributes.x509_issuer: Émetteur PIV/CAC
help_text.requested_attributes.x509_subject: Identité PIV/CAC
-help_text.select_preferred_email: Vous pouvez modifier l’adresse e-mail que vous partagez avec %{sp} car vous possédez plusieurs adresses e-mail associées à votre compte %{app_name}.
+help_text.select_preferred_email_html: Sélectionnez ou ajoutez l’adresse e-mail que vous souhaitez employer pour accéder à %{sp}.
i18n.language: Langue
i18n.locale.en: English
i18n.locale.es: Español
diff --git a/config/locales/zh.yml b/config/locales/zh.yml
index e40a3f9067c..1530d4741cc 100644
--- a/config/locales/zh.yml
+++ b/config/locales/zh.yml
@@ -138,7 +138,7 @@ account.re_verify.footer: 验证身份来查看你的信息。
account.revoke_consent.link_title: 切断连接
account.revoke_consent.longer_description_html: 你的信息将不再与 %{service_provider_html} 分享。以后要访问 %{service_provider_html},你必须授权同意分享你的信息。授权同意请到 %{service_provider_html} 网站并登录。
account.security.link: 请到帮助中心了解更多信息
-account.security.text: 为了你的安全,你的用户资料已被锁住。
+account.security.text: 出于安全考虑,你的用户资料已被锁住。
account.verified_information.address: 地址
account.verified_information.dob: 生日
account.verified_information.full_name: 姓名
@@ -989,11 +989,12 @@ help_text.requested_attributes.full_name: 姓名
help_text.requested_attributes.ial2_reverified_consent_info_html: '因为你重新验证了身份,我们需要得到你的许可才能与 %{sp_html} 分享该信息。'
help_text.requested_attributes.intro_html: 我们会与 %{sp_html} 分享这些信息:
help_text.requested_attributes.phone: 电话号码
+help_text.requested_attributes.select_email_link: 选择电邮
help_text.requested_attributes.social_security_number: 社会保障号码
help_text.requested_attributes.verified_at: 更新是在
help_text.requested_attributes.x509_issuer: PIV/CAC 发放方
help_text.requested_attributes.x509_subject: PIV/CAC 身份
-help_text.select_preferred_email: 因为你有多个电邮与 %{app_name} 账户相关,你可以更改与我们 %{sp} 构分享哪个。
+help_text.select_preferred_email_html: 选择或添加你想用来访问%{sp}的电子邮件。
i18n.language: 语言
i18n.locale.en: English
i18n.locale.es: Español
@@ -1074,7 +1075,7 @@ idv.failure.phone.warning.next_steps_html: 尝试 另一个 你
idv.failure.phone.warning.try_again_button: 尝试另一个号码
idv.failure.phone.warning.you_entered: 你输入了:
idv.failure.sessions.exception: 处理你的请求时内部出错。
-idv.failure.sessions.fail_html: 为了你的安全,我们限制你在网上尝试验证个人信息的次数。 %{timeout}后再试。
+idv.failure.sessions.fail_html: 出于安全考虑,我们限制你在网上尝试验证个人信息的次数。 %{timeout}后再试。
idv.failure.sessions.heading: 我们找不到与你个人信息匹配的记录
idv.failure.sessions.warning: 请检查一下你输入的信息,然后再试一下。常见错误包括社会保障号码或邮编不对。
idv.failure.setup.fail_date_html: 请给我们的联系中心在%{date_html} 之前打电话以继续验证你的身份。
@@ -1447,8 +1448,8 @@ notices.piv_cac_configured: 添加您的政府雇员ID
notices.privacy.privacy_act_statement: 隐私法声明
notices.privacy.security_and_privacy_practices: 安全实践和隐私法声明
notices.resend_confirmation_email.success: 我们发送了另外一个确认电邮。
-notices.session_cleared: 为了你的安全,如果你在 %{minutes} 分钟内不移动到一个新页面,我们会清除你输入的内容。
-notices.session_timedout: 我们已将你登出。为了你的安全,如果你在 %{minutes} 分钟内不移动到一个新页面,%{app_name} 会结束你此次访问。
+notices.session_cleared: 出于安全考虑,如果你在 %{minutes} 分钟内不移动到一个新页面,我们会清除你输入的内容。
+notices.session_timedout: 我们已将你登出。出于安全考虑,如果你在 %{minutes} 分钟内不移动到一个新页面,%{app_name} 会结束你此次访问。
notices.sign_in.recaptcha.disclosure_statement_html: 该网站由 reCAPTCHA 保护而且谷歌的 %{google_policy_link_html} 和 %{google_tos_link_html} 都适用。
notices.signed_up_and_confirmed.first_paragraph_end: 带有确认你的电邮地址的链接。点击链接去继续把这个电邮添加到你的账户。
notices.signed_up_and_confirmed.first_paragraph_start: 我们已发电邮到
@@ -1458,11 +1459,11 @@ notices.signed_up_but_unconfirmed.first_paragraph_start: 我们已发电邮到
notices.signed_up_but_unconfirmed.resend_confirmation_email: 重新发送确认电邮
notices.timeout_warning.partially_signed_in.continue: 继续登录
notices.timeout_warning.partially_signed_in.live_region_message_html: '%{time_left_in_session_html} 后你会被登出。选择“保持我登录状态”保持登录;选择“把我登出”来登出。'
-notices.timeout_warning.partially_signed_in.message_html: 为了你的安全, %{time_left_in_session_html}分钟后我们将取消你的登录。
+notices.timeout_warning.partially_signed_in.message_html: 出于安全考虑, %{time_left_in_session_html}分钟后我们将取消你的登录。
notices.timeout_warning.partially_signed_in.sign_out: 取消登录
notices.timeout_warning.signed_in.continue: 保持我登录状态
notices.timeout_warning.signed_in.live_region_message_html: '%{time_left_in_session_html} 后你会被登出。选择“保持我登录状态”保持登录;选择“把我登出”来登出。'
-notices.timeout_warning.signed_in.message_html: 为了你的安全,我们会在 %{time_left_in_session_html} 后将你登出,除非你告诉我们不要这样做。
+notices.timeout_warning.signed_in.message_html: 出于安全考虑,我们会在 %{time_left_in_session_html} 后将你登出,除非你告诉我们不要这样做。
notices.timeout_warning.signed_in.sign_out: 把我登出
notices.totp_configured: 你账户添加了一个身份证实应用程序。
notices.use_diff_email.link: 使用一个不同的电邮地址
@@ -1570,7 +1571,7 @@ step_indicator.status.complete: 完成了
step_indicator.status.current: 目前步骤
step_indicator.status.not_complete: 未完成
time.am: 上午
-time.formats.event_date: '%B %-d, %Y'
+time.formats.event_date: '%Y年%B%-d日'
time.formats.event_time: '%-l:%M %p'
time.formats.event_timestamp: '%B %-d, %Y at %-l:%M %p'
time.formats.event_timestamp_js: '%{year}年%{month}月%{day}日, %{hour}:%{minute} %{day_period}'
@@ -1698,12 +1699,12 @@ two_factor_authentication.login_options.webauthn: 安全密钥
two_factor_authentication.login_options.webauthn_info: 使用你的安全密钥来访问账户
two_factor_authentication.login_options.webauthn_platform: 人脸或触摸解锁
two_factor_authentication.login_options.webauthn_platform_info: 不用一次性代码,而是用你的面孔或指纹来访问你的账户。
-two_factor_authentication.max_backup_code_login_attempts_reached: 为了你的安全,你的账户暂时被锁住,因为你错误输入备用代码太多次。
-two_factor_authentication.max_generic_login_attempts_reached: 为了你的安全,你的账户暂时被锁住。
-two_factor_authentication.max_otp_login_attempts_reached: 为了你的安全,你的账户暂时被锁住,因为你错误输入一次性代码太多次。
-two_factor_authentication.max_otp_requests_reached: 为了你的安全,你的账户暂时被锁住,因为你要求一次性代码太多次。
-two_factor_authentication.max_personal_key_login_attempts_reached: 为了你的安全,你的账户暂时被锁住,因为你错误输入个人密钥太多次。
-two_factor_authentication.max_piv_cac_login_attempts_reached: 为了你的安全,你的账户暂时被锁住,因为你错误提供 piv/cac 凭据太多次。
+two_factor_authentication.max_backup_code_login_attempts_reached: 出于安全考虑,你的账户暂时被锁住,因为你错误输入备用代码太多次。
+two_factor_authentication.max_generic_login_attempts_reached: 出于安全考虑,你的账户暂时被锁住。
+two_factor_authentication.max_otp_login_attempts_reached: 出于安全考虑,你的账户暂时被锁住,因为你错误输入一次性代码太多次。
+two_factor_authentication.max_otp_requests_reached: 出于安全考虑,你的账户暂时被锁住,因为你要求一次性代码太多次。
+two_factor_authentication.max_personal_key_login_attempts_reached: 出于安全考虑,你的账户暂时被锁住,因为你错误输入个人密钥太多次。
+two_factor_authentication.max_piv_cac_login_attempts_reached: 出于安全考虑,你的账户暂时被锁住,因为你错误提供 PIV/CAC 凭据太多次。
two_factor_authentication.mobile_terms_of_service: 移动服务条款
two_factor_authentication.opt_in.error_retry: 抱歉,我们让你加入有困难。请再试一次。
two_factor_authentication.opt_in.opted_out_html: 你选择不在 %{phone_number_html} 接受短信。你可以选择加入并再在那个电话号码接受安全代码。
diff --git a/config/routes.rb b/config/routes.rb
index 33df2761750..e9f42f70ac7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -360,7 +360,7 @@
get '/document_capture' => 'document_capture#show'
put '/document_capture' => 'document_capture#update'
get '/socure/document_capture' => 'socure/document_capture#show'
- post '/socure/document_capture' => 'socure/document_capture#update'
+ get '/socure/document_capture_update' => 'socure/document_capture#update', as: :socure_document_capture_update
# This route is included in SMS messages sent to users who start the IdV hybrid flow. It
# should be kept short, and should not include underscores ("_").
get '/documents' => 'hybrid_mobile/entry#show', as: :hybrid_mobile_entry
diff --git a/docs/backend.md b/docs/backend.md
index 33494c7ecce..b476f2fd9e0 100644
--- a/docs/backend.md
+++ b/docs/backend.md
@@ -129,7 +129,7 @@ class MyController < ApplicationController
form = MyForm.new(params)
result = form.submit
- analytics.my_event(**result.to_h)
+ analytics.my_event(**result)
if result.success?
do_something(form.sensitive_value_here)
diff --git a/lib/feature_management.rb b/lib/feature_management.rb
index 2808fcc95d7..4d5d4aaef2a 100644
--- a/lib/feature_management.rb
+++ b/lib/feature_management.rb
@@ -125,7 +125,7 @@ def self.recaptcha_enterprise?
IdentityConfig.store.recaptcha_enterprise_project_id.present?
end
- # Whether we collect device profiling information as part of the account creation process
+ # Whether we collect device profiling as part of the account creation process
def self.account_creation_device_profiling_collecting_enabled?
case IdentityConfig.store.account_creation_device_profiling
when :enabled, :collect_only then true
diff --git a/lib/identity_config.rb b/lib/identity_config.rb
index dbdc4874fa7..b859c70528b 100644
--- a/lib/identity_config.rb
+++ b/lib/identity_config.rb
@@ -15,7 +15,7 @@ def self.store
# identity-hostdata transforms these configs to the described type
# rubocop:disable Metrics/BlockLength
- # rubocop:disable Metrics/LineLength
+ # rubocop:disable Layout/LineLength
BUILDER = proc do |config|
# ______________________________________
# / Adding something new in here? Please \
@@ -208,6 +208,7 @@ def self.store
config.add(:in_person_stop_expiring_enrollments, type: :boolean)
config.add(:invalid_gpo_confirmation_zipcode, type: :string)
config.add(:lexisnexis_account_id, type: :string)
+ config.add(:lexisnexis_threatmetrix_authentication_policy, type: :string)
config.add(:lexisnexis_base_url, type: :string)
config.add(:lexisnexis_hmac_auth_enabled, type: :boolean)
config.add(:lexisnexis_hmac_key_id, type: :string)
@@ -471,6 +472,6 @@ def self.store
config.add(:vtm_url)
config.add(:weekly_auth_funnel_report_config, type: :json)
end.freeze
- # rubocop:enable Metrics/LineLength
+ # rubocop:enable Layout/LineLength
# rubocop:enable Metrics/BlockLength
end
diff --git a/lib/tasks/backfill_in_person_pending_at.rake b/lib/tasks/backfill_in_person_pending_at.rake
deleted file mode 100644
index 840bbea85db..00000000000
--- a/lib/tasks/backfill_in_person_pending_at.rake
+++ /dev/null
@@ -1,78 +0,0 @@
-# frozen_string_literal: true
-
-namespace :profiles do
- desc 'Backfill the in_person_verification_pending_at value column.'
-
- ##
- # Usage:
- #
- # Print pending updates
- # bundle exec rake profiles:backfill_in_person_verification_pending_at
- #
- # Commit updates
- # bundle exec rake profiles:backfill_in_person_verification_pending_at UPDATE_PROFILES=true
- #
- task backfill_in_person_verification_pending_at: :environment do |_task, _args|
- ActiveRecord::Base.connection.execute('SET statement_timeout = 60000')
-
- update_profiles = ENV['UPDATE_PROFILES'] == 'true'
-
- profiles = Profile.where(
- deactivation_reason: 'in_person_verification_pending',
- in_person_verification_pending_at: nil,
- )
-
- profiles.each do |profile|
- timestamp = profile.updated_at || profile.created_at
-
- warn "#{profile.id},#{profile.deactivation_reason},#{timestamp}"
- if update_profiles
- profile.update!(
- in_person_verification_pending_at: timestamp,
- deactivation_reason: nil,
- )
- end
- end
- end
-
- ##
- # Usage:
- #
- # Rollback the above:
- #
- # export BACKFILL_OUTPUT=''
- # bundle exec rake profiles:rollback_backfill_in_person_verification_pending_at
- #
- task rollback_backfill_in_person_verification_pending_at: :environment do |_task, _args|
- ActiveRecord::Base.connection.execute('SET statement_timeout = 60000')
-
- profile_data = ENV['BACKFILL_OUTPUT'].split("\n").map do |profile_row|
- profile_row.split(',')
- end
-
- warn "Updating #{profile_data.count} records"
- profile_data.each do |profile_datum|
- profile_id, deactivation_reason, _timestamp = profile_datum
- Profile.where(id: profile_id).update!(
- in_person_verification_pending_at: nil,
- deactivation_reason: deactivation_reason,
- )
- warn profile_id
- end
- end
-
- ##
- # Usage:
- # bundle exec rake profiles:validate_backfill_in_person_verification_pending_at
- #
- task validate_backfill_in_person_verification_pending_at: :environment do |_task, _args|
- ActiveRecord::Base.connection.execute('SET statement_timeout = 60000')
-
- profiles = Profile.where(
- deactivation_reason: 'in_person_verification_pending',
- in_person_verification_pending_at: nil,
- )
-
- warn "backfill_in_person_verification_pending_at left #{profiles.count} rows"
- end
-end
diff --git a/lib/tasks/backfill_sponsor_id.rake b/lib/tasks/backfill_sponsor_id.rake
deleted file mode 100644
index f3463f13440..00000000000
--- a/lib/tasks/backfill_sponsor_id.rake
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-namespace :in_person_enrollments do
- desc 'Backfill the sponsor_id column.'
-
- ##
- # Usage:
- #
- # bundle exec rake in_person_enrollments:backfill_sponsor_id
- #
- task backfill_sponsor_id: :environment do |_task, _args|
- with_timeout do
- ipp_sponsor_id = IdentityConfig.store.usps_ipp_sponsor_id
- enrollments_without_sponsor_id = InPersonEnrollment.where(sponsor_id: nil)
- enrollments_without_sponsor_id_count = enrollments_without_sponsor_id.count
-
- warn("Found #{enrollments_without_sponsor_id_count} in_person_enrollments needing backfill")
-
- tally = 0
- enrollments_without_sponsor_id.in_batches(of: batch_size) do |batch|
- tally += batch.update_all(sponsor_id: ipp_sponsor_id) # rubocop:disable Rails/SkipsModelValidations
- warn("set sponsor_id for #{tally} in_person_enrollments")
- end
- warn("COMPLETE: Updated #{tally} in_person_enrollments")
-
- enrollments_without_sponsor_id = InPersonEnrollment.where(sponsor_id: nil)
- enrollments_without_sponsor_id_count = enrollments_without_sponsor_id.count
- warn("#{enrollments_without_sponsor_id_count} enrollments without a sponsor id")
- end
- end
-
- def batch_size
- ENV['BATCH_SIZE'] ? ENV['BATCH_SIZE'].to_i : 1000
- end
-
- def with_timeout
- timeout_in_seconds ||= if ENV['STATEMENT_TIMEOUT_IN_SECONDS']
- ENV['STATEMENT_TIMEOUT_IN_SECONDS'].to_i.seconds
- else
- 60.seconds
- end
- ActiveRecord::Base.transaction do
- quoted_timeout = ActiveRecord::Base.connection.quote(timeout_in_seconds.in_milliseconds)
- ActiveRecord::Base.connection.execute("SET statement_timeout = #{quoted_timeout}")
- yield
- end
- end
-end
diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb
index 13e7f5b8712..7dca6ffd863 100644
--- a/spec/controllers/idv/document_capture_controller_spec.rb
+++ b/spec/controllers/idv/document_capture_controller_spec.rb
@@ -116,12 +116,14 @@
}
end
+ let(:idv_vendor) { Idp::Constants::Vendors::LEXIS_NEXIS }
+
before do
allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return(
- Idp::Constants::Vendors::LEXIS_NEXIS,
+ idv_vendor,
)
allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return(
- Idp::Constants::Vendors::LEXIS_NEXIS,
+ idv_vendor,
)
end
@@ -130,6 +132,16 @@
expect(assigns(:presenter)).to be_kind_of(Idv::InPerson::UspsFormPresenter)
end
+ context 'when we try to use this controller but we should be using the Socure version' do
+ let(:idv_vendor) { Idp::Constants::Vendors::SOCURE }
+
+ it 'redirects to the Socure controller' do
+ get :show
+
+ expect(response).to redirect_to idv_socure_document_capture_url
+ end
+ end
+
it 'renders the show template' do
expect(subject).to receive(:render).with(
:show,
diff --git a/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb
index 3f58041aa49..d3123a4335e 100644
--- a/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb
+++ b/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb
@@ -15,12 +15,16 @@
let(:document_capture_session_requested_at) { Time.zone.now }
let(:document_capture_session_result_captured_at) { Time.zone.now + 1.second }
let(:document_capture_session_result_success) { true }
+ let(:idv_vendor) { Idp::Constants::Vendors::MOCK }
before do
stub_analytics
session[:doc_capture_user_id] = user&.id
session[:document_capture_session_uuid] = document_capture_session_uuid
+
+ allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return(idv_vendor)
+ allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return(idv_vendor)
end
describe 'before_actions' do
@@ -55,6 +59,16 @@
}
end
+ context 'when we try to use this controller but we should be using the Socure version' do
+ let(:idv_vendor) { Idp::Constants::Vendors::SOCURE }
+
+ it 'redirects to the Socure controller' do
+ get :show
+
+ expect(response).to redirect_to idv_hybrid_mobile_socure_document_capture_url
+ end
+ end
+
it 'renders the show template' do
expect(subject).to receive(:render).with(
:show,
diff --git a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb
index 4fd74fcd0c7..d87e5061368 100644
--- a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb
+++ b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb
@@ -4,7 +4,7 @@
include FlowPolicyHelper
let(:idv_vendor) { Idp::Constants::Vendors::SOCURE }
- let(:fake_socure_endpoint) { 'https://fake-socure.com' }
+ let(:fake_socure_endpoint) { 'https://fake-socure.test' }
let(:user) { create(:user) }
let(:stored_result) { nil }
let(:socure_enabled) { true }
@@ -63,8 +63,17 @@
end
end
+ context 'when we try to use this controller but we should be using the LN/mock version' do
+ let(:idv_vendor) { Idp::Constants::Vendors::LEXIS_NEXIS }
+
+ it 'redirects to the LN/mock controller' do
+ get :show
+ expect(response).to redirect_to idv_hybrid_mobile_document_capture_url
+ end
+ end
+
context 'happy path' do
- let(:response_redirect_url) { 'https://idv.test/dance' }
+ let(:socure_capture_app_url) { 'https://verify.socure.test/' }
let(:docv_transaction_token) { '176dnc45d-2e34-46f3-82217-6f540ae90673' }
let(:response_body) do
{
@@ -74,7 +83,7 @@
customerUserId: document_capture_session_uuid,
docvTransactionToken: docv_transaction_token,
qrCode: 'data:image/png;base64,iVBO......K5CYII=',
- url: response_redirect_url,
+ url: socure_capture_app_url,
},
}
end
@@ -82,6 +91,7 @@
before do
allow(I18n).to receive(:locale).and_return(expected_language)
allow(request_class).to receive(:new).and_call_original
+ allow(request_class).to receive(:handle_connection_error).and_call_original
get(:show)
end
@@ -94,6 +104,11 @@
)
end
+ it 'sets DocumentCaptureSession socure_docv_capture_app_url value' do
+ document_capture_session.reload
+ expect(document_capture_session.socure_docv_capture_app_url).to eq(socure_capture_app_url)
+ end
+
context 'language is english' do
let(:expected_language) { :en }
@@ -105,7 +120,7 @@
config: {
documentType: 'license',
redirect: {
- method: 'POST',
+ method: 'GET',
url: idv_hybrid_mobile_socure_document_capture_url,
},
language: expected_language,
@@ -128,7 +143,7 @@
config: {
documentType: 'license',
redirect: {
- method: 'POST',
+ method: 'GET',
url: idv_hybrid_mobile_socure_document_capture_url,
},
language: 'zh-cn',
@@ -143,9 +158,9 @@
context 'renders the interstital page' do
render_views
- it 'it includes the socure redirect url' do
+ it 'response includes the socure capture app url' do
expect(response).to have_http_status 200
- expect(response.body).to have_link(href: response_redirect_url)
+ expect(response.body).to have_link(href: socure_capture_app_url)
end
it 'puts the docvTransactionToken into the document capture session' do
@@ -175,6 +190,74 @@
expect(response).to be_not_found
end
end
+
+ context 'when socure error encountered' do
+ let(:fake_socure_endpoint) { 'https://fake-socure.test/' }
+ let(:failed_response_body) do
+ { 'status' => 'Error',
+ 'referenceId' => '1cff6d33-1cc0-4205-b740-c9a9e6b8bd66',
+ 'data' => {},
+ 'msg' => 'No active account is associated with this request' }
+ end
+ let(:response_body_401) do
+ {
+ status: 'Error',
+ referenceId: '7ff0cdc5-395e-45d1-8467-0ff1b41c11dc',
+ msg: 'string',
+ }
+ end
+ let(:no_doc_found_response_body) do
+ {
+ referenceId: '0dc21b0d-04df-4dd5-8533-ec9ecdafe0f4',
+ msg: {
+ status: 400,
+ msg: 'No Documents found',
+ },
+ }
+ end
+ before do
+ allow(IdentityConfig.store).to receive(:socure_document_request_endpoint).
+ and_return(fake_socure_endpoint)
+ end
+ it 'connection timeout still responds to user' do
+ stub_request(:post, fake_socure_endpoint).to_raise(Faraday::ConnectionFailed)
+ get(:show)
+ expect(response).to redirect_to(idv_unavailable_path)
+ end
+
+ it 'socure error response still gives a result to user' do
+ stub_request(:post, fake_socure_endpoint).to_return(
+ status: 401,
+ body: JSON.generate(failed_response_body),
+ )
+ get(:show)
+ expect(response).to redirect_to(idv_unavailable_path)
+ end
+ it 'socure nil response still gives a result to user' do
+ stub_request(:post, fake_socure_endpoint).to_return(
+ status: 500,
+ body: nil,
+ )
+ get(:show)
+ expect(response).to redirect_to(idv_unavailable_path)
+ end
+ it 'socure nil response still gives a result to user' do
+ stub_request(:post, fake_socure_endpoint).to_return(
+ status: 401,
+ body: JSON.generate(response_body_401),
+ )
+ get(:show)
+ expect(response).to redirect_to(idv_unavailable_path)
+ end
+ it 'socure nil response still gives a result to user' do
+ stub_request(:post, fake_socure_endpoint).to_return(
+ status: 401,
+ body: JSON.generate(no_doc_found_response_body),
+ )
+ get(:show)
+ expect(response).to redirect_to(idv_unavailable_path)
+ end
+ end
end
describe '#update' do
diff --git a/spec/controllers/idv/socure/document_capture_controller_spec.rb b/spec/controllers/idv/socure/document_capture_controller_spec.rb
index 67525862117..6ecf9d88f05 100644
--- a/spec/controllers/idv/socure/document_capture_controller_spec.rb
+++ b/spec/controllers/idv/socure/document_capture_controller_spec.rb
@@ -4,9 +4,19 @@
include FlowPolicyHelper
let(:idv_vendor) { Idp::Constants::Vendors::SOCURE }
- let(:fake_socure_endpoint) { 'https://fake-socure.com' }
+ let(:fake_socure_endpoint) { 'https://fake-socure.test' }
let(:user) { create(:user) }
- let(:stored_result) { nil }
+ let(:doc_auth_success) { true }
+ let(:stored_result) do
+ DocumentCaptureSessionResult.new(
+ id: SecureRandom.uuid,
+ success: doc_auth_success,
+ doc_auth_success: doc_auth_success,
+ selfie_status: :none,
+ pii: { first_name: 'Testy', last_name: 'Testerson' },
+ attention_with_barcode: false,
+ )
+ end
let(:socure_enabled) { true }
let(:document_capture_session) do
@@ -23,6 +33,7 @@
and_return(fake_socure_endpoint)
allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return(idv_vendor)
allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return(idv_vendor)
+ allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user)
allow(subject).to receive(:stored_result).and_return(stored_result)
@@ -65,8 +76,17 @@
subject.idv_session.document_capture_session_uuid = expected_uuid
end
+ context 'when we try to use this controller but we should be using the LN/mock version' do
+ let(:idv_vendor) { Idp::Constants::Vendors::LEXIS_NEXIS }
+
+ it 'redirects to the LN/mock controller' do
+ get :show
+ expect(response).to redirect_to idv_document_capture_url
+ end
+ end
+
context 'happy path' do
- let(:response_redirect_url) { 'https://idv.test/dance' }
+ let(:socure_capture_app_url) { 'https://verify.socure.test/' }
let(:docv_transaction_token) { '176dnc45d-2e34-46f3-82217-6f540ae90673' }
let(:response_body) do
{
@@ -76,7 +96,7 @@
customerUserId: '121212',
docvTransactionToken: docv_transaction_token,
qrCode: 'data:image/png;base64,iVBO......K5CYII=',
- url: response_redirect_url,
+ url: socure_capture_app_url,
},
}
end
@@ -84,6 +104,7 @@
before do
allow(request_class).to receive(:new).and_call_original
allow(I18n).to receive(:locale).and_return(expected_language)
+ allow(DocumentCaptureSession).to receive(:find_by).and_return(document_capture_session)
get(:show)
end
@@ -91,11 +112,16 @@
expect(request_class).to have_received(:new).
with(
document_capture_session_uuid: expected_uuid,
- redirect_url: idv_socure_document_capture_url,
+ redirect_url: idv_socure_document_capture_update_url,
language: expected_language,
)
end
+ it 'sets DocumentCaptureSession socure_docv_capture_app_url value' do
+ document_capture_session.reload
+ expect(document_capture_session.socure_docv_capture_app_url).to eq(socure_capture_app_url)
+ end
+
context 'language is english' do
let(:expected_language) { :en }
@@ -107,8 +133,8 @@
config: {
documentType: 'license',
redirect: {
- method: 'POST',
- url: idv_socure_document_capture_url,
+ method: 'GET',
+ url: idv_socure_document_capture_update_url,
},
language: :en,
},
@@ -130,8 +156,8 @@
config: {
documentType: 'license',
redirect: {
- method: 'POST',
- url: idv_socure_document_capture_url,
+ method: 'GET',
+ url: idv_socure_document_capture_update_url,
},
language: 'zh-cn',
},
@@ -145,9 +171,9 @@
context 'renders the interstital page' do
render_views
- it 'it includes the socure redirect url' do
+ it 'response includes the socure capture app url' do
expect(response).to have_http_status 200
- expect(response.body).to have_link(href: response_redirect_url)
+ expect(response.body).to have_link(href: socure_capture_app_url)
end
it 'puts the docvTransactionToken into the document capture session' do
@@ -177,13 +203,91 @@
expect(response).to be_not_found
end
end
+
+ context 'when socure error encountered' do
+ let(:fake_socure_endpoint) { 'https://fake-socure.test/' }
+ let(:failed_response_body) do
+ { 'status' => 'Error',
+ 'referenceId' => '1cff6d33-1cc0-4205-b740-c9a9e6b8bd66',
+ 'data' => {},
+ 'msg' => 'No active account is associated with this request' }
+ end
+ let(:response_body_401) do
+ {
+ status: 'Error',
+ referenceId: '7ff0cdc5-395e-45d1-8467-0ff1b41c11dc',
+ msg: 'string',
+ }
+ end
+ let(:no_doc_found_response_body) do
+ {
+ referenceId: '0dc21b0d-04df-4dd5-8533-ec9ecdafe0f4',
+ msg: {
+ status: 400,
+ msg: 'No Documents found',
+ },
+ }
+ end
+ before do
+ allow(IdentityConfig.store).to receive(:socure_document_request_endpoint).
+ and_return(fake_socure_endpoint)
+ end
+ it 'connection timeout still responds to user' do
+ stub_request(:post, fake_socure_endpoint).to_raise(Faraday::ConnectionFailed)
+ get(:show)
+ expect(response).to redirect_to(idv_unavailable_path)
+ end
+
+ it 'socure error response still gives a result to user' do
+ stub_request(:post, fake_socure_endpoint).to_return(
+ status: 401,
+ body: JSON.generate(failed_response_body),
+ )
+ get(:show)
+ expect(response).to redirect_to(idv_unavailable_path)
+ end
+ it 'socure nil response still gives a result to user' do
+ stub_request(:post, fake_socure_endpoint).to_return(
+ status: 500,
+ body: nil,
+ )
+ get(:show)
+ expect(response).to redirect_to(idv_unavailable_path)
+ end
+ it 'socure nil response still gives a result to user' do
+ stub_request(:post, fake_socure_endpoint).to_return(
+ status: 401,
+ body: JSON.generate(response_body_401),
+ )
+ get(:show)
+ expect(response).to redirect_to(idv_unavailable_path)
+ end
+ it 'socure nil response still gives a result to user' do
+ stub_request(:post, fake_socure_endpoint).to_return(
+ status: 401,
+ body: JSON.generate(no_doc_found_response_body),
+ )
+ get(:show)
+ expect(response).to redirect_to(idv_unavailable_path)
+ end
+ end
end
describe '#update' do
it 'returns OK (200)' do
- post(:update)
+ get(:update)
- expect(response).to have_http_status(:ok)
+ expect(response).to redirect_to(idv_ssn_path)
+ end
+
+ context 'when doc auth fails' do
+ let(:doc_auth_success) { false }
+
+ it 'redirects to document capture' do
+ get(:update)
+
+ expect(response).to redirect_to(idv_socure_document_capture_path)
+ end
end
context 'when socure is disabled' do
diff --git a/spec/controllers/sign_up/passwords_controller_spec.rb b/spec/controllers/sign_up/passwords_controller_spec.rb
index bf20d2ce5eb..9d910e9bf57 100644
--- a/spec/controllers/sign_up/passwords_controller_spec.rb
+++ b/spec/controllers/sign_up/passwords_controller_spec.rb
@@ -3,6 +3,47 @@
RSpec.describe SignUp::PasswordsController do
let(:token) { 'new token' }
+ describe '#new' do
+ let!(:user) { create(:user, :unconfirmed, confirmation_token: token) }
+ subject(:response) { get :new, params: { confirmation_token: token } }
+
+ it 'flashes a message informing the user that they need to set a password' do
+ response
+
+ expect(flash.now[:success]).to eq(t('devise.confirmations.confirmed_but_must_set_password'))
+ end
+
+ it 'processes valid token' do
+ expect(controller).to receive(:process_valid_confirmation_token)
+
+ response
+ end
+
+ it 'assigns variables expected to be available in the view' do
+ response
+
+ expect(assigns(:password_form)).to be_instance_of(PasswordForm)
+ expect(assigns(:email_address)).to be_instance_of(EmailAddress)
+ expect(assigns(:forbidden_passwords)).to be_present.and all be_kind_of(String)
+ expect(assigns(:confirmation_token)).to be_kind_of(String)
+ end
+
+ context 'with invalid confirmation_token' do
+ let!(:user) do
+ create(
+ :user,
+ :unconfirmed,
+ confirmation_token: token,
+ confirmation_sent_at: (IdentityConfig.store.add_email_link_valid_for_hours + 1).hours.ago,
+ )
+ end
+
+ it 'redirects to sign up page' do
+ expect(response).to redirect_to(sign_up_register_url)
+ end
+ end
+ end
+
describe '#create' do
subject(:response) { post :create, params: params }
let(:params) do
@@ -28,12 +69,6 @@
it 'tracks analytics' do
subject
- expect(@analytics).to have_logged_event(
- 'User Registration: Email Confirmation',
- success: true,
- errors: {},
- user_id: user.uuid,
- )
expect(@analytics).to have_logged_event(
'Password Creation',
success: true,
@@ -77,12 +112,6 @@
it 'tracks an invalid password event' do
subject
- expect(@analytics).to have_logged_event(
- 'User Registration: Email Confirmation',
- errors: {},
- success: true,
- user_id: user.uuid,
- )
expect(@analytics).to have_logged_event(
'Password Creation',
success: false,
@@ -111,12 +140,6 @@
it 'tracks invalid password_confirmation error' do
subject
- expect(@analytics).to have_logged_event(
- 'User Registration: Email Confirmation',
- errors: {},
- success: true,
- user_id: user.uuid,
- )
expect(@analytics).to have_logged_event(
'Password Creation',
success: false,
@@ -161,22 +184,4 @@
end
end
end
-
- describe '#new' do
- render_views
-
- it 'rejects when confirmation_token is invalid' do
- invalid_confirmation_sent_at =
- Time.zone.now - (IdentityConfig.store.add_email_link_valid_for_hours.hours.in_seconds + 1)
- create(
- :user,
- :unconfirmed,
- confirmation_token: token,
- confirmation_sent_at: invalid_confirmation_sent_at,
- )
-
- get :new, params: { confirmation_token: token }
- expect(response).to redirect_to(sign_up_register_url)
- end
- end
end
diff --git a/spec/controllers/sign_up/registrations_controller_spec.rb b/spec/controllers/sign_up/registrations_controller_spec.rb
index b815b87fd1d..c4611e90ffa 100644
--- a/spec/controllers/sign_up/registrations_controller_spec.rb
+++ b/spec/controllers/sign_up/registrations_controller_spec.rb
@@ -56,6 +56,36 @@
)
end
end
+
+ context 'with threatmetrix enabled' do
+ let(:tmx_session_id) { '1234' }
+
+ before do
+ allow(FeatureManagement).to receive(:account_creation_device_profiling_collecting_enabled?).
+ and_return(true)
+ allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_org_id).and_return('org1')
+ allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_mock_enabled).
+ and_return(false)
+ subject.session[:threatmetrix_session_id] = tmx_session_id
+ end
+
+ it 'renders new valid request' do
+ tmx_url = 'https://h.online-metrix.net/fp'
+ expect(subject).to receive(:render).with(
+ :new,
+ formats: :html,
+ locals: { threatmetrix_session_id: tmx_session_id,
+ threatmetrix_javascript_urls:
+ ["#{tmx_url}/tags.js?org_id=org1&session_id=#{tmx_session_id}"],
+ threatmetrix_iframe_url:
+ "#{tmx_url}/tags?org_id=org1&session_id=#{tmx_session_id}" },
+ ).and_call_original
+
+ get :new
+
+ expect(response).to render_template(:new)
+ end
+ end
end
describe '#create' do
@@ -172,5 +202,34 @@
expect(response).to render_template(:new)
end
+
+ context 'with threatmetrix enabled' do
+ let(:tmx_session_id) { '1234' }
+
+ before do
+ allow(FeatureManagement).to receive(:account_creation_device_profiling_collecting_enabled?).
+ and_return(true)
+ allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_org_id).and_return('org1')
+ allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_mock_enabled).
+ and_return(false)
+ subject.session[:threatmetrix_session_id] = tmx_session_id
+ end
+
+ it 'renders new with invalid request' do
+ tmx_url = 'https://h.online-metrix.net/fp'
+ expect(subject).to receive(:render).with(
+ :new,
+ locals: { threatmetrix_session_id: tmx_session_id,
+ threatmetrix_javascript_urls:
+ ["#{tmx_url}/tags.js?org_id=org1&session_id=#{tmx_session_id}"],
+ threatmetrix_iframe_url:
+ "#{tmx_url}/tags?org_id=org1&session_id=#{tmx_session_id}" },
+ ).and_call_original
+
+ post :create, params: params.deep_merge(user: { email: 'invalid@' })
+
+ expect(response).to render_template(:new)
+ end
+ end
end
end
diff --git a/spec/controllers/sign_up/select_email_controller_spec.rb b/spec/controllers/sign_up/select_email_controller_spec.rb
index 2e9a841af3d..73adfe3dbb4 100644
--- a/spec/controllers/sign_up/select_email_controller_spec.rb
+++ b/spec/controllers/sign_up/select_email_controller_spec.rb
@@ -59,6 +59,16 @@
expect(response).to be_not_found
end
end
+
+ context 'with only one verified email address' do
+ let(:user) { create(:user) }
+
+ it 'redirects to the sign up completed path' do
+ response
+
+ expect(response).to redirect_to(sign_up_completed_path)
+ end
+ end
end
describe '#create' do
diff --git a/spec/controllers/users/backup_code_setup_controller_spec.rb b/spec/controllers/users/backup_code_setup_controller_spec.rb
index 5a6ca70cb1d..e5c8f26596d 100644
--- a/spec/controllers/users/backup_code_setup_controller_spec.rb
+++ b/spec/controllers/users/backup_code_setup_controller_spec.rb
@@ -15,11 +15,20 @@
end
shared_examples 'valid backup codes creation' do
+ let(:threatmetrix_attrs) do
+ {
+ user_id: user.id,
+ request_ip: Faker::Internet.ip_v4_address,
+ threatmetrix_session_id: 'test-session',
+ email: user.email,
+ }
+ end
+
it 'creates backup codes and logs expected events' do
stub_analytics
allow(controller).to receive(:in_multi_mfa_selection_flow?).and_return(true)
- Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics)
+ Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics, threatmetrix_attrs)
expect(PushNotification::HttpPush).to receive(:deliver).
with(PushNotification::RecoveryInformationChangedEvent.new(user: user))
diff --git a/spec/controllers/users/email_confirmations_controller_spec.rb b/spec/controllers/users/email_confirmations_controller_spec.rb
index c6a7613e381..8d3197ad185 100644
--- a/spec/controllers/users/email_confirmations_controller_spec.rb
+++ b/spec/controllers/users/email_confirmations_controller_spec.rb
@@ -87,5 +87,58 @@
expect(flash[:error]).to eq t('errors.messages.confirmation_invalid_token')
end
end
+
+ describe '#process_successful_confirmation' do
+ let(:user) { create(:user) }
+
+ context 'adding an email from the account page' do
+ before do
+ stub_sign_in(user)
+ end
+
+ it 'redirects to the account page' do
+ new_email = Faker::Internet.email
+
+ add_email_form = AddUserEmailForm.new
+ add_email_form.submit(user, email: new_email)
+ email_record = add_email_form.email_address_record(new_email)
+
+ get :create, params: { confirmation_token: email_record.reload.confirmation_token }
+
+ expect(response).to redirect_to(account_url)
+ end
+ end
+
+ context 'adding an email from the service provider consent flow' do
+ let(:confirmation_token) { 'token' }
+ let(:sp_request_uuid) { 'request-id' }
+ let(:request_id_param) {}
+
+ before do
+ stub_sign_in(user)
+ ServiceProviderRequestProxy.create(
+ issuer: 'http://localhost:3000',
+ url: '',
+ uuid: sp_request_uuid,
+ ial: '1',
+ acr_values: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF,
+ )
+ end
+
+ it 'adds an email from the service provider consent flow' do
+ new_email = Faker::Internet.email
+ add_email_form = AddUserEmailForm.new
+ add_email_form.submit(user, email: new_email, request_id: sp_request_uuid)
+ email_record = add_email_form.email_address_record(new_email)
+
+ get :create, params: {
+ confirmation_token: email_record.reload.confirmation_token,
+ request_id: sp_request_uuid,
+ }
+
+ expect(response).to redirect_to(sign_up_select_email_url)
+ end
+ end
+ end
end
end
diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb
index 2756a6a9815..d6416d45ae4 100644
--- a/spec/controllers/users/webauthn_setup_controller_spec.rb
+++ b/spec/controllers/users/webauthn_setup_controller_spec.rb
@@ -88,6 +88,15 @@
}
end
+ let(:threatmetrix_attrs) do
+ {
+ user_id: user.id,
+ request_ip: Faker::Internet.ip_v4_address,
+ threatmetrix_session_id: 'test-session',
+ email: user.email,
+ }
+ end
+
before do
allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:3000')
request.host = 'localhost:3000'
@@ -95,7 +104,7 @@
end
it 'tracks the submission' do
- Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics)
+ Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics, threatmetrix_attrs)
patch :confirm, params: params
@@ -229,12 +238,21 @@
}
end
+ let(:threatmetrix_attrs) do
+ {
+ user_id: user.id,
+ request_ip: Faker::Internet.ip_v4_address,
+ threatmetrix_session_id: 'test-session',
+ email: user.email,
+ }
+ end
+
before do
controller.user_session[:in_account_creation_flow] = true
end
it 'should log expected events' do
- Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics)
+ Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics, threatmetrix_attrs)
patch :confirm, params: params
@@ -383,8 +401,17 @@
controller.user_session[:mfa_attempts] = { auth_method: 'webauthn', attempts: 1 }
end
+ let(:threatmetrix_attrs) do
+ {
+ user_id: user.id,
+ request_ip: Faker::Internet.ip_v4_address,
+ threatmetrix_session_id: 'test-session',
+ email: user.email,
+ }
+ end
+
it 'tracks the submission' do
- Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics)
+ Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics, threatmetrix_attrs)
patch :confirm, params: params
diff --git a/spec/features/account_connected_apps_spec.rb b/spec/features/account_connected_apps_spec.rb
index b3be84b37a5..70a0254b563 100644
--- a/spec/features/account_connected_apps_spec.rb
+++ b/spec/features/account_connected_apps_spec.rb
@@ -87,7 +87,7 @@
expect(page).to have_field(user.email) { |field| !field[:checked] }
choose user.email
- click_on t('help_text.requested_attributes.change_email_link')
+ click_on t('help_text.requested_attributes.select_email_link')
within('li', text: identity.display_name) do
expect(page).not_to have_content(t('account.connected_apps.email_not_selected'))
@@ -97,7 +97,7 @@
expect(page).to have_field(user.email) { |field| field[:checked] }
- click_on(t('help_text.requested_attributes.change_email_link'))
+ click_on(t('help_text.requested_attributes.select_email_link'))
expect(page).to have_content strip_tags(
t('account.connected_apps.email_update_success_html', sp_name: identity.display_name),
diff --git a/spec/features/account_creation/threat_metrix_spec.rb b/spec/features/account_creation/threat_metrix_spec.rb
new file mode 100644
index 00000000000..0a17582b3d1
--- /dev/null
+++ b/spec/features/account_creation/threat_metrix_spec.rb
@@ -0,0 +1,39 @@
+require 'rails_helper'
+
+RSpec.feature 'ThreatMetrix in account creation', :js do
+ before do
+ allow(IdentityConfig.store).to receive(:account_creation_device_profiling).and_return(:enabled)
+ allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_org_id).and_return('test_org')
+ end
+
+ it 'logs the threatmetrix result once the account is fully registered' do
+ visit root_url
+ click_on t('links.create_account')
+ fill_in t('forms.registration.labels.email'), with: Faker::Internet.email
+ check t('sign_up.terms', app_name: APP_NAME)
+ select 'Reject', from: :mock_profiling_result
+ click_button t('forms.buttons.submit.default')
+ user = confirm_last_user
+ set_password(user)
+ fake_analytics = FakeAnalytics.new
+ expect_any_instance_of(AccountCreationThreatMetrixJob).to receive(:analytics).with(user).
+ and_return(fake_analytics)
+ select_2fa_option('backup_code')
+ click_continue
+
+ expect(fake_analytics).to have_logged_event(
+ :account_creation_tmx_result,
+ account_lex_id: 'super-cool-test-lex-id',
+ errors: { review_status: ['reject'] },
+ response_body: {
+ **JSON.parse(LexisNexisFixtures.ddp_success_redacted_response_json),
+ 'review_status' => 'reject',
+ },
+ review_status: 'reject',
+ session_id: 'super-cool-test-session-id',
+ success: true,
+ timed_out: false,
+ transaction_id: 'ddp-mock-transaction-id-123',
+ )
+ end
+end
diff --git a/spec/features/idv/hybrid_mobile/entry_spec.rb b/spec/features/idv/hybrid_mobile/entry_spec.rb
index 9b252f78109..4261ce82806 100644
--- a/spec/features/idv/hybrid_mobile/entry_spec.rb
+++ b/spec/features/idv/hybrid_mobile/entry_spec.rb
@@ -22,6 +22,10 @@
let(:link_to_visit) { link_sent_via_sms }
context 'valid link' do
+ before do
+ allow(IdentityConfig.store).to receive(:socure_enabled).and_return(true)
+ end
+
it 'puts the user on the document capture page' do
expect(link_to_visit).to be
@@ -29,6 +33,11 @@
visit link_to_visit
# Should have redirected to the actual doc capture url
expect(current_url).to eql(idv_hybrid_mobile_document_capture_url)
+
+ # Confirm that we end up on the LN / Mock page even if we try to
+ # go to the Socure one.
+ visit idv_hybrid_mobile_socure_document_capture_url
+ expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url)
end
end
end
diff --git a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb
index 9456bb8a772..5b53406d281 100644
--- a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb
+++ b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb
@@ -10,6 +10,7 @@
before do
allow(FeatureManagement).to receive(:doc_capture_polling_enabled?).and_return(true)
+ allow(IdentityConfig.store).to receive(:socure_enabled).and_return(true)
allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
allow(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config|
@sms_link = config[:link]
@@ -44,7 +45,11 @@
# Confirm that jumping to LinkSent page does not cause errors
visit idv_link_sent_url
expect(page).to have_current_path(root_url)
- visit idv_hybrid_mobile_document_capture_url
+
+ # Confirm that we end up on the LN / Mock page even if we try to
+ # go to the Socure one.
+ visit idv_hybrid_mobile_socure_document_capture_url
+ expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url)
# Confirm that clicking cancel and then coming back doesn't cause errors
click_link 'Cancel'
diff --git a/spec/features/multiple_emails/sp_sign_in_spec.rb b/spec/features/multiple_emails/sp_sign_in_spec.rb
index 33b0a5fc6f8..5b9f6e33992 100644
--- a/spec/features/multiple_emails/sp_sign_in_spec.rb
+++ b/spec/features/multiple_emails/sp_sign_in_spec.rb
@@ -36,7 +36,7 @@
choose emails.second
- click_button(t('help_text.requested_attributes.change_email_link'))
+ click_button(t('help_text.requested_attributes.select_email_link'))
expect(current_path).to eq(sign_up_completed_path)
click_agree_and_continue
@@ -55,7 +55,7 @@
click_submit_default
click_link(t('help_text.requested_attributes.change_email_link'))
choose email2.email
- click_button(t('help_text.requested_attributes.change_email_link'))
+ click_button(t('help_text.requested_attributes.select_email_link'))
expect(current_path).to eq(sign_up_completed_path)
click_agree_and_continue
click_submit_default
@@ -103,7 +103,7 @@
click_link(t('help_text.requested_attributes.change_email_link'))
choose emails.second
- click_button(t('help_text.requested_attributes.change_email_link'))
+ click_button(t('help_text.requested_attributes.select_email_link'))
expect(current_path).to eq(sign_up_completed_path)
@@ -127,7 +127,7 @@
click_submit_default_twice
click_link(t('help_text.requested_attributes.change_email_link'))
choose email2.email
- click_button(t('help_text.requested_attributes.change_email_link'))
+ click_button(t('help_text.requested_attributes.select_email_link'))
expect(current_path).to eq(sign_up_completed_path)
click_agree_and_continue
click_submit_default
diff --git a/spec/features/visitors/email_confirmation_spec.rb b/spec/features/visitors/email_confirmation_spec.rb
index 1925545deca..299234123c6 100644
--- a/spec/features/visitors/email_confirmation_spec.rb
+++ b/spec/features/visitors/email_confirmation_spec.rb
@@ -18,6 +18,7 @@
end
scenario 'confirms valid email and sets valid password' do
+ stub_analytics
reset_email
email = 'test@example.com'
sign_up_with(email)
@@ -28,6 +29,9 @@
expect(page).to have_title t('titles.confirmations.show')
expect(page).to have_content t('forms.confirmation.show_hdr')
+ # Regression: Previously, this event had been logged multiple times per confirmation.
+ expect(@analytics).to have_logged_event('User Registration: Email Confirmation').once
+
fill_in t('forms.password'), with: Features::SessionHelper::VALID_PASSWORD
fill_in t('components.password_confirmation.confirm_label'),
with: Features::SessionHelper::VALID_PASSWORD
diff --git a/spec/fixtures/proofing/lexis_nexis/ddp/account_creation_request.json b/spec/fixtures/proofing/lexis_nexis/ddp/account_creation_request.json
new file mode 100644
index 00000000000..9d638cc1748
--- /dev/null
+++ b/spec/fixtures/proofing/lexis_nexis/ddp/account_creation_request.json
@@ -0,0 +1,27 @@
+{
+ "api_key": "test_api_key",
+ "org_id": "test_org_id",
+ "account_email": "test@example.com",
+ "event_type": "ACCOUNT_CREATION",
+ "policy": "test-authentication-policy",
+ "service_type": "all",
+ "session_id": "UNIQUE_SESSION_ID",
+ "input_ip_address": "127.0.0.1",
+ "account_address_street1": "",
+ "account_address_street2": "",
+ "account_address_city": "",
+ "account_address_state": "",
+ "account_address_country": "",
+ "account_address_zip": "",
+ "account_date_of_birth": "",
+ "account_first_name": "",
+ "account_last_name": "",
+ "account_telephone": "",
+ "account_drivers_license_issuer": "",
+ "account_drivers_license_number": "",
+ "account_drivers_license_type": "",
+ "national_id_number": "",
+ "national_id_type": "",
+ "local_attrib_1": ""
+ }
+
\ No newline at end of file
diff --git a/spec/fixtures/proofing/lexis_nexis/ddp/error_response.json b/spec/fixtures/proofing/lexis_nexis/ddp/error_response.json
index 0de8a11bf99..0fea4b30420 100644
--- a/spec/fixtures/proofing/lexis_nexis/ddp/error_response.json
+++ b/spec/fixtures/proofing/lexis_nexis/ddp/error_response.json
@@ -4,4 +4,4 @@
"request_result":"fail_invalid_parameter",
"review_status":"REVIEW_STATUS",
"tmx_summary_reason_code": ["Identity_Negative_History"]
-}
+}
\ No newline at end of file
diff --git a/spec/fixtures/proofing/lexis_nexis/ddp/failed_response.json b/spec/fixtures/proofing/lexis_nexis/ddp/failed_response.json
new file mode 100644
index 00000000000..891067d6928
--- /dev/null
+++ b/spec/fixtures/proofing/lexis_nexis/ddp/failed_response.json
@@ -0,0 +1,7 @@
+{
+ "error_detail": "service_type",
+ "request_id":"1234-abcd",
+ "request_result":"fail_invalid_parameter",
+ "review_status":"reject",
+ "tmx_summary_reason_code": ["Identity_Negative_History"]
+}
diff --git a/spec/helpers/threat_metrix_helper_spec.rb b/spec/helpers/threat_metrix_helper_spec.rb
new file mode 100644
index 00000000000..d0cf955a2b0
--- /dev/null
+++ b/spec/helpers/threat_metrix_helper_spec.rb
@@ -0,0 +1,77 @@
+require 'rails_helper'
+
+RSpec.describe ThreatMetrixHelper do
+ include ThreatMetrixHelper
+
+ describe '#threatmetrix_javascript_urls' do
+ let(:session_id) { '1234' }
+ before do
+ allow(IdentityConfig.store).
+ to receive(:lexisnexis_threatmetrix_org_id).
+ and_return('test_id')
+
+ allow(Rails.application.config.asset_sources).to receive(:get_sources).
+ with('mock-device-profiling').and_return(['/mock-device-profiling.js'])
+ end
+ context 'mock is enabled' do
+ before do
+ allow(IdentityConfig.store).
+ to receive(:lexisnexis_threatmetrix_mock_enabled).
+ and_return(true)
+ end
+ it 'should return mock config source' do
+ sources = threatmetrix_javascript_urls(session_id)
+ expect(sources).to eq(['/mock-device-profiling.js?org_id=test_id&session_id=1234'])
+ end
+ end
+ context 'mock is not enabled' do
+ before do
+ allow(IdentityConfig.store).
+ to receive(:lexisnexis_threatmetrix_mock_enabled).
+ and_return(false)
+ end
+ it 'should return actual url' do
+ javascript_sources = threatmetrix_javascript_urls(session_id)
+ expect(javascript_sources).
+ to eq(['https://h.online-metrix.net/fp/tags.js?org_id=test_id&session_id=1234'])
+ end
+ end
+ end
+
+ describe '#threatmetrix_iframe_url' do
+ let(:session_id) { '1234' }
+ before do
+ allow(IdentityConfig.store).
+ to receive(:lexisnexis_threatmetrix_org_id).
+ and_return('test_id')
+
+ allow(Rails.application.config.asset_sources).to receive(:get_sources).
+ with('mock-device-profiling').and_return(['/mock-device-profiling.js'])
+ end
+ context 'mock is enabled' do
+ before do
+ allow(IdentityConfig.store).
+ to receive(:lexisnexis_threatmetrix_mock_enabled).
+ and_return(true)
+ end
+ it 'should return mock javascript config' do
+ iframe_sources = threatmetrix_iframe_url(session_id)
+ expect(iframe_sources).
+ to eq('http://www.example.com/test/device_profiling?org_id=test_id&session_id=1234')
+ end
+ end
+
+ context 'mock is not enabled' do
+ before do
+ allow(IdentityConfig.store).
+ to receive(:lexisnexis_threatmetrix_mock_enabled).
+ and_return(false)
+ end
+ it 'should return mock config source' do
+ iframe_sources = threatmetrix_iframe_url(session_id)
+ expect(iframe_sources).
+ to eq('https://h.online-metrix.net/fp/tags?org_id=test_id&session_id=1234')
+ end
+ end
+ end
+end
diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb
index 370714691d6..220d6842536 100644
--- a/spec/i18n_spec.rb
+++ b/spec/i18n_spec.rb
@@ -70,7 +70,6 @@ class BaseTask
{ key: 'simple_form.no', locales: %i[es] }, # "No" is "No" in Spanish
{ key: 'telephony.format_length.six', locales: %i[zh] }, # numeral is not translated
{ key: 'telephony.format_length.ten', locales: %i[zh] }, # numeral is not translated
- { key: 'time.formats.event_date', locales: %i[es zh] },
{ key: 'time.formats.event_time', locales: %i[es zh] },
{ key: 'time.formats.event_timestamp', locales: %i[zh] },
{ key: 'time.formats.full_date', locales: %i[es] }, # format is the same in Spanish and English
diff --git a/spec/jobs/account_creation_threat_metrix_job_spec.rb b/spec/jobs/account_creation_threat_metrix_job_spec.rb
new file mode 100644
index 00000000000..cffb09cfda7
--- /dev/null
+++ b/spec/jobs/account_creation_threat_metrix_job_spec.rb
@@ -0,0 +1,118 @@
+require 'rails_helper'
+
+RSpec.describe AccountCreationThreatMetrixJob, type: :job do
+ let(:user) { create(:user, :fully_registered) }
+ let(:request_ip) { Faker::Internet.ip_v4_address }
+ let(:threatmetrix_session_id) { SecureRandom.uuid }
+ let(:authentication_device_profiling) { :collect_only }
+ let(:lexisnexis_threatmetrix_mock_enabled) { false }
+ let(:threatmetrix_response) { LexisNexisFixtures.ddp_success_response_json }
+ let(:threatmetrix_stub) { stub_threatmetrix_request(threatmetrix_response) }
+ let(:job_analytics) { FakeAnalytics.new }
+
+ before do
+ allow(IdentityConfig.store).to receive(:account_creation_device_profiling).
+ and_return(authentication_device_profiling)
+ allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_mock_enabled).
+ and_return(lexisnexis_threatmetrix_mock_enabled)
+ allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_base_url).
+ and_return('https://www.example.com')
+ allow(instance).to receive(:analytics).and_return(job_analytics)
+ end
+
+ describe '#perform' do
+ let(:instance) { AccountCreationThreatMetrixJob.new }
+
+ subject(:perform) do
+ instance.perform(
+ user_id: user.id,
+ threatmetrix_session_id: threatmetrix_session_id,
+ request_ip: request_ip,
+ )
+ end
+
+ context 'Threat Metrix Account Creation analysis passes' do
+ let(:threatmetrix_response) { LexisNexisFixtures.ddp_success_response_json }
+ it 'logs a successful result' do
+ threatmetrix_stub
+
+ perform
+
+ expect(job_analytics).to have_logged_event(
+ :account_creation_tmx_result,
+ hash_including(
+ success: true,
+ review_status: 'pass',
+ ),
+ )
+ end
+ end
+
+ context 'with an error response result' do
+ let(:threatmetrix_response) { LexisNexisFixtures.ddp_failure_response_json }
+
+ it 'stores an unsuccessful result' do
+ threatmetrix_stub
+
+ perform
+
+ expect(job_analytics).to have_logged_event(
+ :account_creation_tmx_result,
+ hash_including(
+ success: false,
+ review_status: 'reject',
+ ),
+ )
+ end
+ end
+
+ context 'with threatmetrix disabled' do
+ let(:authentication_device_profiling) { :disabled }
+
+ it 'does not make a request to threatmetrix' do
+ threatmetrix_stub
+
+ perform
+
+ expect(threatmetrix_stub).to_not have_been_requested
+ expect(job_analytics).to have_logged_event(
+ :account_creation_tmx_result,
+ hash_including(
+ success: true,
+ client: 'tmx_disabled',
+ review_status: 'pass',
+ ),
+ )
+ end
+ end
+
+ context 'without a threatmetrix session ID' do
+ let(:threatmetrix_session_id) { nil }
+ let(:ipp_enrollment_in_progress) { false }
+ let(:threatmetrix_response) { LexisNexisFixtures.ddp_failure_response_json }
+
+ it 'does not make a request to threatmetrix' do
+ threatmetrix_stub
+
+ perform
+
+ expect(threatmetrix_stub).to_not have_been_requested
+ expect(job_analytics).to have_logged_event(
+ :account_creation_tmx_result,
+ hash_including(
+ success: false,
+ client: 'tmx_session_id_missing',
+ review_status: 'reject',
+ ),
+ )
+ end
+ end
+ end
+
+ def stub_threatmetrix_request(threatmetrix_response)
+ stub_request(
+ :post,
+ 'https://www.example.com/api/session-query',
+ ).to_return(body: threatmetrix_response)
+ end
+end
diff --git a/spec/lib/tasks/backfill_sponsor_id_rake_spec.rb b/spec/lib/tasks/backfill_sponsor_id_rake_spec.rb
deleted file mode 100644
index 9f26042b370..00000000000
--- a/spec/lib/tasks/backfill_sponsor_id_rake_spec.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-require 'rails_helper'
-require 'rake'
-
-RSpec.describe 'in_person_enrollments:backfill_sponsor_id rake task' do
- let!(:task) do
- Rake.application.rake_require 'tasks/backfill_sponsor_id'
- Rake::Task.define_task(:environment)
- Rake::Task['in_person_enrollments:backfill_sponsor_id']
- end
-
- subject(:invoke_task) do
- actual_stderr = $stderr
- proxy_stderr = StringIO.new
- begin
- $stderr = proxy_stderr
- task.reenable
- task.invoke
- proxy_stderr.string
- ensure
- $stderr = actual_stderr
- end
- end
-
- let(:pending_enrollment) { create(:in_person_enrollment, :pending) }
- let(:expired_enrollment) { create(:in_person_enrollment, :expired) }
- let(:failed_enrollment) { create(:in_person_enrollment, :failed) }
- let(:enrollment_with_service_provider) do
- create(:in_person_enrollment, :with_service_provider)
- end
- let(:enrollment_with_sponsor_id) { create(:in_person_enrollment) }
-
- before do
- allow(IdentityConfig.store).to receive(:usps_ipp_sponsor_id).and_return('31459')
- end
-
- it 'does not change the value of an existing sponsor id' do
- original_sponsor_id = enrollment_with_sponsor_id.sponsor_id
- subject
- expect(enrollment_with_sponsor_id.sponsor_id).to eq(original_sponsor_id)
- end
-
- it 'sets a sponsor id that is a string' do
- subject
- enrollments = InPersonEnrollment.all
- enrollments.each do |enrollment|
- expect(enrollment.sponsor_id).to be_a String
- end
- end
-end
diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb
index ac6dc41e16e..5aa21140cc6 100644
--- a/spec/mailers/previews/user_mailer_preview.rb
+++ b/spec/mailers/previews/user_mailer_preview.rb
@@ -136,7 +136,8 @@ def verify_by_mail_letter_requested
end
def add_email
- UserMailer.with(user: user, email_address: email_address_record).add_email(SecureRandom.hex)
+ UserMailer.with(user: user, email_address: email_address_record).
+ add_email(token: SecureRandom.hex, request_id: nil)
end
def email_added
diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb
index 71f615af5fe..fef65205fe3 100644
--- a/spec/mailers/user_mailer_spec.rb
+++ b/spec/mailers/user_mailer_spec.rb
@@ -32,7 +32,10 @@
describe '#add_email' do
let(:token) { SecureRandom.hex }
- let(:mail) { UserMailer.with(user: user, email_address: email_address).add_email(token) }
+ let(:mail) do
+ UserMailer.with(user: user, email_address: email_address).
+ add_email(token: token, request_id: nil, from_select_email_flow: nil)
+ end
it_behaves_like 'a system email'
it_behaves_like 'an email that respects user email locale preference'
@@ -47,7 +50,8 @@
context 'when user adds email from select email flow' do
let(:token) { SecureRandom.hex }
let(:mail) do
- UserMailer.with(user: user, email_address: email_address).add_email(token, true)
+ UserMailer.with(user: user, email_address: email_address).
+ add_email(token: token, request_id: nil, from_select_email_flow: true)
end
it 'renders the add_email_confirmation_url' do
diff --git a/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb b/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb
index 21bb5c2e455..aae5df981fb 100644
--- a/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb
+++ b/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb
@@ -23,21 +23,33 @@
)
end
subject(:presenter) { described_class.new(enrollment: enrollment) }
- describe '#formatted_due_date' do
- subject(:formatted_due_date) { presenter.formatted_due_date }
-
- around do |example|
- Time.use_zone('UTC') { example.run }
- end
- it 'returns a formatted due date' do
- expect(formatted_due_date).to eq 'August 12, 2023'
+ describe '#formatted_due_date' do
+ let(:enrollment_established_at) { DateTime.new(2024, 7, 5) }
+
+ context 'when the enrollment has an enrollment_established_at time' do
+ [
+ ['English', :en, 'August 3, 2024'],
+ ['Spanish', :es, '3 de agosto de 2024'],
+ ['French', :fr, '3 août 2024'],
+ ['Chinese', :zh, '2024年8月3日'],
+ ].each do |language, locale, expected|
+ context "when locale is #{language}" do
+ before do
+ I18n.locale = locale
+ end
+
+ it "returns the formatted due date in #{language}" do
+ expect(presenter.formatted_due_date).to eq(expected)
+ end
+ end
+ end
end
- context 'there is no enrollment_established_at' do
+ context 'when the enrollment does not have an enrollment_established_at time' do
let(:enrollment_established_at) { nil }
it 'returns formatted due date when no enrollment_established_at' do
- expect(formatted_due_date).to eq 'July 13, 2023'
+ expect(presenter.formatted_due_date).to eq 'July 13, 2023'
end
end
end
diff --git a/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb b/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb
index e320151a0a7..563a1f3d620 100644
--- a/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb
+++ b/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb
@@ -31,13 +31,25 @@
end
describe '#formatted_verified_date' do
- around do |example|
- Time.use_zone('UTC') { example.run }
+ before do
+ enrollment.update(status_updated_at: DateTime.new(2024, 7, 5))
end
- it 'returns a formatted verified date' do
- enrollment.update(status_updated_at: status_updated_at)
- expect(presenter.formatted_verified_date).to eq 'July 13, 2022'
+ [
+ ['English', :en, 'July 4, 2024'],
+ ['Spanish', :es, '4 de julio de 2024'],
+ ['French', :fr, '4 juillet 2024'],
+ ['Chinese', :zh, '2024年7月4日'],
+ ].each do |language, locale, expected|
+ context "when locale is #{language}" do
+ before do
+ I18n.locale = locale
+ end
+
+ it "returns the formatted due date in #{language}" do
+ expect(presenter.formatted_verified_date).to eq(expected)
+ end
+ end
end
end
diff --git a/spec/services/account_creation/device_profiling_spec.rb b/spec/services/account_creation/device_profiling_spec.rb
new file mode 100644
index 00000000000..3178f51170e
--- /dev/null
+++ b/spec/services/account_creation/device_profiling_spec.rb
@@ -0,0 +1,66 @@
+require 'rails_helper'
+
+RSpec.describe AccountCreation::DeviceProfiling do
+ let(:threatmetrix_session_id) { '13232' }
+ let(:threatmetrix_proofer_result) do
+ instance_double(Proofing::DdpResult, success?: true, transaction_id: 'ddp-123')
+ end
+ let(:threatmetrix_proofer) do
+ instance_double(
+ Proofing::LexisNexis::Ddp::Proofer,
+ proof: threatmetrix_proofer_result,
+ )
+ end
+
+ subject(:device_profiling) { described_class.new }
+
+ describe '#proof' do
+ before do
+ allow(device_profiling).to receive(:proofer).and_return(threatmetrix_proofer)
+ end
+
+ subject(:result) do
+ device_profiling.proof(
+ request_ip: Faker::Internet.ip_v4_address,
+ threatmetrix_session_id: threatmetrix_session_id,
+ user_email: Faker::Internet.email,
+ )
+ end
+
+ context 'ThreatMetrix is enabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:account_creation_device_profiling).
+ and_return(:collect_only)
+ allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_mock_enabled).
+ and_return(false)
+ end
+
+ context 'session id is missing' do
+ let(:threatmetrix_session_id) { nil }
+
+ it 'does not make a request to the ThreatMetrix proofer' do
+ result
+ expect(threatmetrix_proofer).not_to have_received(:proof)
+ end
+
+ it 'returns a failed result' do
+ expect(result.success?).to be(false)
+ expect(result.client).to eq('tmx_session_id_missing')
+ expect(result.review_status).to eq('reject')
+ end
+ end
+
+ context 'valid threatmetrix input' do
+ it 'makes a request to the ThreatMetrix proofer' do
+ result
+ expect(threatmetrix_proofer).to have_received(:proof)
+ end
+
+ it 'returns a passed result' do
+ expect(result.success?).to be(true)
+ expect(result.transaction_id).to eq('ddp-123')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/doc_auth/socure/request_spec.rb b/spec/services/doc_auth/socure/request_spec.rb
index feedb8318f8..10f2cc1fad1 100644
--- a/spec/services/doc_auth/socure/request_spec.rb
+++ b/spec/services/doc_auth/socure/request_spec.rb
@@ -10,7 +10,7 @@
end
describe '#fetch' do
- let(:fake_socure_endpoint) { 'https://fake-socure.com/' }
+ let(:fake_socure_endpoint) { 'https://fake-socure.test/' }
let(:fake_metric_name) { 'fake metric' }
before do
@@ -40,8 +40,10 @@
let(:response) { nil }
let(:response_status) { 403 }
- it 'returns {}' do
- expect(request.fetch).to eq({})
+ # Because we have not implemented handle_connection_error at this level
+ # (defined in docv_result and document_request)
+ it 'raises a NotImplementedError' do
+ expect { request.fetch }.to raise_error NotImplementedError
end
end
end
diff --git a/spec/services/doc_auth/socure/requests/document_request_spec.rb b/spec/services/doc_auth/socure/requests/document_request_spec.rb
index 46db7c5ed90..17c8032118b 100644
--- a/spec/services/doc_auth/socure/requests/document_request_spec.rb
+++ b/spec/services/doc_auth/socure/requests/document_request_spec.rb
@@ -15,7 +15,7 @@
describe '#fetch' do
let(:document_type) { 'license' }
- let(:fake_socure_endpoint) { 'https://fake-socure.com/' }
+ let(:fake_socure_endpoint) { 'https://fake-socure.test/' }
let(:fake_socure_document_capture_app_url) { 'https://verify.socure.us/something' }
let(:docv_transaction_token) { 'fake docv transaction token' }
let(:fake_socure_response) do
@@ -38,7 +38,7 @@
documentType: document_type,
redirect:
{
- method: 'POST',
+ method: 'GET',
url: redirect_url,
},
language: language,
@@ -99,5 +99,31 @@
expect { document_request.fetch }.not_to raise_error
end
end
+ context 'with timeout exception' do
+ let(:response) { nil }
+ let(:response_status) { 403 }
+ let(:faraday_connection_failed_exception) { Faraday::ConnectionFailed }
+
+ before do
+ stub_request(:post, fake_socure_endpoint).to_raise(faraday_connection_failed_exception)
+ end
+ it 'expect handle_connection_error method to be called' do
+ connection_error_attributes = {
+ success: false,
+ errors: { network: true },
+ exception: faraday_connection_failed_exception,
+ extra: {
+ vendor: 'Socure',
+ vendor_status_code: nil,
+ vendor_status_message: nil,
+ }.compact,
+ }
+ result = document_request.fetch
+ expect(result[:success]).to eq(connection_error_attributes[:success])
+ expect(result[:errors]).to eq(connection_error_attributes[:errors])
+ expect(result[:exception]).to be_a Faraday::ConnectionFailed
+ expect(result[:extra]).to eq(connection_error_attributes[:extra])
+ end
+ end
end
end
diff --git a/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb b/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb
new file mode 100644
index 00000000000..2fe380431d8
--- /dev/null
+++ b/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb
@@ -0,0 +1,60 @@
+require 'rails_helper'
+
+RSpec.describe DocAuth::Socure::Requests::DocvResultRequest do
+ let(:document_capture_session_uuid) { 'fake uuid' }
+ let(:biometric_comparison_required) { false }
+
+ subject(:docv_result_request) do
+ described_class.new(
+ document_capture_session_uuid:,
+ biometric_comparison_required: biometric_comparison_required,
+ )
+ end
+
+ describe '#fetch' do
+ let(:fake_socure_endpoint) { 'https://fake-socure.test/' }
+ let(:fake_socure_api_endpoint) { 'https://fake-socure.test/api/3.0/EmailAuthScore' }
+ let(:docv_transaction_token) { 'fake docv transaction token' }
+ let(:user) { create(:user) }
+ let(:document_capture_session) do
+ DocumentCaptureSession.create(user:).tap do |dcs|
+ dcs.socure_docv_transaction_token = docv_transaction_token
+ end
+ end
+
+ before do
+ allow(IdentityConfig.store).to receive(:socure_idplus_base_url).
+ and_return(fake_socure_endpoint)
+ allow(DocumentCaptureSession).to receive(:find_by).and_return(document_capture_session)
+ end
+
+ context 'with socure failures' do
+ let(:fake_socure_response) { {} }
+ let(:fake_socure_status) { 500 }
+
+ it 'expect correct doc auth response during a connection failure' do
+ stub_request(:post, fake_socure_api_endpoint).to_raise(Faraday::ConnectionFailed)
+ response_hash = docv_result_request.fetch.to_h
+ expect(response_hash[:success]).to eq(false)
+ expect(response_hash[:errors]).to eq({ network: true })
+ expect(response_hash[:vendor]).to eq('Socure')
+ expect(response_hash[:exception]).to be_a(Faraday::ConnectionFailed)
+ end
+
+ it 'expect correct doc auth response for a socure fail response' do
+ stub_request(:post, fake_socure_api_endpoint).
+ to_return(
+ status: fake_socure_status,
+ body: JSON.generate(fake_socure_response),
+ )
+ response_hash = docv_result_request.fetch.to_h
+ expect(response_hash[:success]).to eq(false)
+ expect(response_hash[:errors]).to eq({ network: true })
+ expect(response_hash[:errors]).to eq({ network: true })
+ expect(response_hash[:vendor]).to eq('Socure')
+ expect(response_hash[:exception]).to be_a(DocAuth::RequestError)
+ expect(response_hash[:exception].message).to include('Unexpected HTTP response 500')
+ end
+ end
+ end
+end
diff --git a/spec/services/form_response_spec.rb b/spec/services/form_response_spec.rb
index 5efcefb17ac..dbd1880aa64 100644
--- a/spec/services/form_response_spec.rb
+++ b/spec/services/form_response_spec.rb
@@ -273,6 +273,21 @@
end
end
+ describe '#to_hash' do
+ it 'allows for splatting response as alias of #to_h' do
+ errors = ActiveModel::Errors.new(build_stubbed(:user))
+ errors.add(:email_language, :blank, message: 'Language cannot be blank')
+ response = FormResponse.new(success: false, errors:, serialize_error_details_only: true)
+
+ expect(**response).to eq(
+ success: false,
+ error_details: {
+ email_language: { blank: true },
+ },
+ )
+ end
+ end
+
describe '#extra' do
it 'returns the extra hash' do
extra = { foo: 'bar' }
diff --git a/spec/services/funnel/registration/add_mfa_spec.rb b/spec/services/funnel/registration/add_mfa_spec.rb
index 58d977ff077..d6f21fc19ed 100644
--- a/spec/services/funnel/registration/add_mfa_spec.rb
+++ b/spec/services/funnel/registration/add_mfa_spec.rb
@@ -3,20 +3,39 @@
RSpec.describe Funnel::Registration::AddMfa do
let(:analytics) { FakeAnalytics.new }
subject { described_class }
+ let(:user) { create(:user) }
- let(:user_id) do
- user = create(:user)
- user.id
- end
+ let(:user_id) { user.id }
let(:funnel) { RegistrationLog.first }
+ let(:threatmetrix_attrs) do
+ {
+ user_id: user_id,
+ request_ip: Faker::Internet.ip_v4_address,
+ threatmetrix_session_id: SecureRandom.uuid,
+ email: user.email,
+ }
+ end
+
it 'shows user is not fully registered with no mfa' do
expect(funnel&.registered_at).to_not be_present
end
it 'shows user is fully registered after adding an mfa' do
- subject.call(user_id, 'phone', analytics)
+ subject.call(user_id, 'phone', analytics, threatmetrix_attrs)
expect(funnel.registered_at).to be_present
end
+
+ context 'with threat metrix for account creation enabled' do
+ before do
+ allow(FeatureManagement).
+ to receive(:account_creation_device_profiling_collecting_enabled?).
+ and_return(:collect_only)
+ end
+ it 'triggers threatmetrix job call' do
+ expect(AccountCreationThreatMetrixJob).to receive(:perform_later)
+ subject.call(user_id, 'phone', analytics, threatmetrix_attrs)
+ end
+ end
end
diff --git a/spec/services/funnel/registration/total_registered_count_spec.rb b/spec/services/funnel/registration/total_registered_count_spec.rb
index 39fa0db3280..11d8efd5b9f 100644
--- a/spec/services/funnel/registration/total_registered_count_spec.rb
+++ b/spec/services/funnel/registration/total_registered_count_spec.rb
@@ -1,7 +1,16 @@
require 'rails_helper'
RSpec.describe Funnel::Registration::TotalRegisteredCount do
+ let(:user) { create(:user) }
let(:analytics) { FakeAnalytics.new }
+ let(:threatmetrix_attrs) do
+ {
+ user_id: user.id,
+ request_ip: Faker::Internet.ip_v4_address,
+ threatmetrix_session_id: 'test-session',
+ email: user.email,
+ }
+ end
subject { described_class }
it 'returns 0' do
@@ -9,13 +18,12 @@
end
it 'returns 0 until the user is fully registered' do
- user = create(:user)
user_id = user.id
expect(Funnel::Registration::TotalRegisteredCount.call).to eq(0)
expect(Funnel::Registration::TotalRegisteredCount.call).to eq(0)
- Funnel::Registration::AddMfa.call(user_id, 'phone', analytics)
+ Funnel::Registration::AddMfa.call(user_id, 'phone', analytics, threatmetrix_attrs)
expect(Funnel::Registration::TotalRegisteredCount.call).to eq(1)
end
@@ -36,6 +44,6 @@
def register_user
user = create(:user)
user_id = user.id
- Funnel::Registration::AddMfa.call(user_id, 'backup_codes', analytics)
+ Funnel::Registration::AddMfa.call(user_id, 'backup_codes', analytics, threatmetrix_attrs)
end
end
diff --git a/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb b/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb
index c57e331ba28..0f9485bf8b1 100644
--- a/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb
+++ b/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
RSpec.describe Proofing::LexisNexis::Ddp::Proofer do
- let(:applicant) do
+ let(:proofing_applicant) do
{
first_name: 'Testy',
last_name: 'McTesterson',
@@ -22,10 +22,25 @@
}
end
- let(:verification_request) do
+ let(:authentication_applicant) do
+ {
+ threatmetrix_session_id: '123456',
+ email: 'test@example.com',
+ request_ip: '127.0.0.1',
+ }
+ end
+
+ let(:proofing_verification_request) do
+ Proofing::LexisNexis::Ddp::VerificationRequest.new(
+ applicant: proofing_applicant,
+ config: LexisNexisFixtures.example_ddp_proofing_config,
+ )
+ end
+
+ let(:authentication_verification_request) do
Proofing::LexisNexis::Ddp::VerificationRequest.new(
- applicant: applicant,
- config: LexisNexisFixtures.example_config,
+ applicant: authentication_applicant,
+ config: LexisNexisFixtures.example_ddp_authentication_config,
)
end
@@ -40,10 +55,10 @@
it 'raises a timeout error' do
stub_request(
:post,
- verification_request.url,
+ proofing_verification_request.url,
).to_timeout
- expect { verification_request.send_request }.to raise_error(
+ expect { proofing_verification_request.send_request }.to raise_error(
Proofing::TimeoutError,
'LexisNexis timed out waiting for verification response',
)
@@ -55,24 +70,24 @@
request =
stub_request(
:post,
- verification_request.url,
+ proofing_verification_request.url,
).with(
- body: verification_request.body,
- headers: verification_request.headers,
+ body: proofing_verification_request.body,
+ headers: proofing_verification_request.headers,
).to_return(
body: LexisNexisFixtures.ddp_success_response_json,
status: 200,
)
- verification_request.send_request
+ proofing_verification_request.send_request
expect(request).to have_been_requested.once
end
end
end
- subject do
- described_class.new(LexisNexisFixtures.example_config.to_h)
+ subject(:proofer) do
+ described_class.new(LexisNexisFixtures.example_ddp_proofing_config.to_h)
end
describe '#proof' do
@@ -84,56 +99,102 @@
)
stub_request(
:post,
- verification_request.url,
+ proofing_verification_request.url,
).to_return(
body: response_body,
status: 200,
)
end
- context 'when the response is a full match' do
- let(:response_body) { LexisNexisFixtures.ddp_success_response_json }
+ context 'when user is going through Idv' do
+ context 'when the response is a full match' do
+ let(:response_body) { LexisNexisFixtures.ddp_success_response_json }
- it 'is a successful result' do
- result = subject.proof(applicant)
+ it 'is a successful result' do
+ result = proofer.proof(proofing_applicant)
- expect(result.success?).to eq(true)
- expect(result.errors).to be_empty
- expect(result.review_status).to eq('pass')
- expect(result.session_id).to eq('super-cool-test-session-id')
- expect(result.account_lex_id).to eq('super-cool-test-lex-id')
+ expect(result.success?).to eq(true)
+ expect(result.errors).to be_empty
+ expect(result.review_status).to eq('pass')
+ expect(result.session_id).to eq('super-cool-test-session-id')
+ expect(result.account_lex_id).to eq('super-cool-test-lex-id')
+ end
end
- end
- context 'when the response raises an exception' do
- let(:response_body) { '' }
+ context 'when the response raises an exception' do
+ let(:response_body) { '' }
- it 'returns an exception result' do
- error = RuntimeError.new('hi')
+ it 'returns an exception result' do
+ error = RuntimeError.new('hi')
- expect(NewRelic::Agent).to receive(:notice_error).with(error)
+ expect(NewRelic::Agent).to receive(:notice_error).with(error)
- stub_request(
- :post,
- verification_request.url,
- ).to_raise(error)
+ stub_request(
+ :post,
+ proofing_verification_request.url,
+ ).to_raise(error)
- result = subject.proof(applicant)
+ result = proofer.proof(proofing_applicant)
- expect(result.success?).to eq(false)
- expect(result.errors).to be_empty
- expect(result.exception).to eq(error)
+ expect(result.success?).to eq(false)
+ expect(result.errors).to be_empty
+ expect(result.exception).to eq(error)
+ end
+ end
+
+ context 'when the review status has an unexpected value' do
+ let(:response_body) { LexisNexisFixtures.ddp_unexpected_review_status_response_json }
+
+ it 'returns an exception result' do
+ result = proofer.proof(proofing_applicant)
+
+ expect(result.success?).to eq(false)
+ expect(result.exception.inspect).
+ to include(LexisNexisFixtures.ddp_unexpected_review_status)
+ end
end
end
- context 'when the review status has an unexpected value' do
- let(:response_body) { LexisNexisFixtures.ddp_unexpected_review_status_response_json }
+ context 'when user is going through account creation' do
+ subject(:proofer) do
+ described_class.new(LexisNexisFixtures.example_ddp_authentication_config.to_h)
+ end
+
+ before do
+ allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_authentication_policy).
+ and_return('test-authentication-policy')
+ end
+ context 'when the response is a full match' do
+ let(:response_body) { LexisNexisFixtures.ddp_success_response_json }
+
+ it 'is a successful result' do
+ result = proofer.proof(authentication_applicant)
+
+ expect(result.success?).to eq(true)
+ expect(result.errors).to be_empty
+ expect(result.review_status).to eq('pass')
+ end
+ end
+
+ context 'when the response raises an exception' do
+ let(:response_body) { '' }
+
+ it 'returns an exception result' do
+ error = RuntimeError.new('hi')
+
+ expect(NewRelic::Agent).to receive(:notice_error).with(error)
+
+ stub_request(
+ :post,
+ authentication_verification_request.url,
+ ).to_raise(error)
- it 'returns an exception result' do
- result = subject.proof(applicant)
+ result = proofer.proof(authentication_applicant)
- expect(result.success?).to eq(false)
- expect(result.exception.inspect).to include(LexisNexisFixtures.ddp_unexpected_review_status)
+ expect(result.success?).to eq(false)
+ expect(result.errors).to be_empty
+ expect(result.exception).to eq(error)
+ end
end
end
end
diff --git a/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb b/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb
index 8541278f1a4..199cef5e19a 100644
--- a/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb
+++ b/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb
@@ -15,8 +15,8 @@
zipcode: '70802-12345',
state_id_number: '12345678',
state_id_jurisdiction: 'LA',
- threatmetrix_session_id: 'UNIQUE_SESSION_ID',
phone: '5551231234',
+ threatmetrix_session_id: 'UNIQUE_SESSION_ID',
email: 'test@example.com',
request_ip: '127.0.0.1',
uuid_prefix: 'ABCD',
@@ -25,29 +25,61 @@
let(:response_body) { LexisNexisFixtures.ddp_success_response_json }
subject do
- described_class.new(applicant: applicant, config: LexisNexisFixtures.example_ddp_config)
+ described_class.new(
+ applicant: applicant,
+ config: LexisNexisFixtures.example_ddp_proofing_config,
+ )
end
before do
allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_policy).
and_return('test-policy')
+ allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_authentication_policy).
+ and_return('test-authentication-policy')
end
describe '#body' do
- it 'returns a properly formed request body' do
- expect(subject.body).to eq(LexisNexisFixtures.ddp_request_json)
+ context 'Idv verification request' do
+ it 'returns a properly formed request body' do
+ response_json = JSON.parse(subject.body)
+ expected_json = JSON.parse(LexisNexisFixtures.ddp_request_json)
+ expect(response_json).to eq(expected_json)
+ end
+
+ context 'without an address line 2' do
+ let(:applicant) do
+ hash = super()
+ hash.delete(:address2)
+ hash
+ end
+
+ it 'sets StreetAddress2 to and empty string' do
+ parsed_body = JSON.parse(subject.body, symbolize_names: true)
+ expect(parsed_body[:account_address_street2]).to eq('')
+ end
+ end
end
- context 'without an address line 2' do
+ context 'Authentication verification request' do
let(:applicant) do
- hash = super()
- hash.delete(:address2)
- hash
+ {
+ threatmetrix_session_id: 'UNIQUE_SESSION_ID',
+ email: 'test@example.com',
+ request_ip: '127.0.0.1',
+ }
+ end
+
+ subject do
+ described_class.new(
+ applicant: applicant,
+ config: LexisNexisFixtures.example_ddp_authentication_config,
+ )
end
- it 'sets StreetAddress2 to and empty string' do
- parsed_body = JSON.parse(subject.body, symbolize_names: true)
- expect(parsed_body[:account_address_street2]).to eq('')
+ it 'returns a properly formed request body' do
+ response_json = JSON.parse(subject.body)
+ expected_json = JSON.parse(LexisNexisFixtures.ddp_authentication_request_json)
+ expect(response_json).to eq(expected_json)
end
end
end
diff --git a/spec/services/send_add_email_confirmation_spec.rb b/spec/services/send_add_email_confirmation_spec.rb
new file mode 100644
index 00000000000..722b6b97422
--- /dev/null
+++ b/spec/services/send_add_email_confirmation_spec.rb
@@ -0,0 +1,36 @@
+require 'rails_helper'
+
+RSpec.describe SendAddEmailConfirmation do
+ subject(:instance) { described_class.new(user) }
+
+ describe '#call' do
+ subject(:result) { instance.call(email_address:, request_id:, in_select_email_flow:) }
+
+ let(:user) { create(:user, confirmed_at: nil) }
+ let(:email_address) { user.email_addresses.take }
+ let(:request_id) { '1234-abcd' }
+ let(:in_select_email_flow) { nil }
+ let(:confirmation_token) { 'confirm-me' }
+
+ before do
+ allow(Devise).to receive(:friendly_token).once.and_return(confirmation_token)
+ email_address.update!(
+ confirmed_at: nil,
+ confirmation_token: nil,
+ confirmation_sent_at: nil,
+ )
+ end
+ it 'sends the user an email with a confirmation link and the request id' do
+ email_address.update!(confirmed_at: Time.zone.now)
+
+ result
+
+ expect_delivered_email_count(1)
+ expect_delivered_email(
+ to: [user.email_addresses.first.email],
+ subject: t('user_mailer.add_email.subject'),
+ body: [request_id],
+ )
+ end
+ end
+end
diff --git a/spec/support/analytics_helper.rb b/spec/support/analytics_helper.rb
index 1c2ae7fe23a..90920f23576 100644
--- a/spec/support/analytics_helper.rb
+++ b/spec/support/analytics_helper.rb
@@ -2,13 +2,15 @@ module AnalyticsHelper
def stub_analytics(user: nil)
analytics = FakeAnalytics.new
- if user
- allow(controller).to receive(:analytics).and_wrap_original do |original|
- expect(original.call.user).to eq(user)
- analytics
- end
- else
- controller.analytics = analytics
+ stub = if defined?(controller)
+ allow(controller)
+ else
+ allow_any_instance_of(ApplicationController)
+ end
+
+ stub.to receive(:analytics).and_wrap_original do |original|
+ expect(original.call.user).to eq(user) if user
+ analytics
end
@analytics = analytics
diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb
index 60ecb049ee1..9c9ba9f72d5 100644
--- a/spec/support/features/session_helper.rb
+++ b/spec/support/features/session_helper.rb
@@ -127,8 +127,7 @@ def sign_up
confirm_last_user
end
- def sign_up_and_set_password
- user = sign_up
+ def set_password(user)
user.password = VALID_PASSWORD
fill_in t('forms.password'), with: user.password
fill_in t('components.password_confirmation.confirm_label'), with: user.password
@@ -136,6 +135,10 @@ def sign_up_and_set_password
user
end
+ def sign_up_and_set_password
+ set_password(sign_up)
+ end
+
def sign_in_user(user = create(:user), email = nil)
email ||= user.email_addresses.first.email
signin(email, user.password)
diff --git a/spec/support/lexis_nexis_fixtures.rb b/spec/support/lexis_nexis_fixtures.rb
index 4eb114cf331..38bcd8545bf 100644
--- a/spec/support/lexis_nexis_fixtures.rb
+++ b/spec/support/lexis_nexis_fixtures.rb
@@ -16,14 +16,29 @@ def example_config
)
end
- def example_ddp_config
+ def example_ddp_proofing_config
Proofing::LexisNexis::Config.new(
api_key: 'test_api_key',
base_url: 'https://example.com',
org_id: 'test_org_id',
+ ddp_policy: 'test-policy',
)
end
+ def example_ddp_authentication_config
+ Proofing::LexisNexis::Config.new(
+ api_key: 'test_api_key',
+ base_url: 'https://example.com',
+ org_id: 'test_org_id',
+ ddp_policy: 'test-authentication-policy',
+ )
+ end
+
+ def ddp_authentication_request_json
+ raw = read_fixture_file_at_path('ddp/account_creation_request.json')
+ JSON.parse(raw).to_json
+ end
+
def ddp_request_json
raw = read_fixture_file_at_path('ddp/request.json')
JSON.parse(raw).to_json
@@ -44,11 +59,6 @@ def ddp_failure_response_json
JSON.parse(raw).to_json
end
- def ddp_error_response_json
- raw = read_fixture_file_at_path('ddp/error_response.json')
- JSON.parse(raw).to_json
- end
-
def ddp_unexpected_review_status
'unexpected_review_status_that_causes_problems'
end
diff --git a/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb b/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb
index 8fc3f51dd18..4645965a8ed 100644
--- a/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb
+++ b/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb
@@ -27,9 +27,6 @@
inputs = page.find_all('[type="radio"]')
expect(inputs.count).to eq(2)
expect(inputs).to be_logically_grouped(t('titles.select_email'))
- fieldset = page.find('fieldset')
- expect(fieldset).to have_description(
- t('help_text.select_preferred_email', sp: identity.display_name, app_name: APP_NAME),
- )
+ expect(rendered).to have_content(identity.display_name)
end
end