diff --git a/.erb-lint.yml b/.erb_lint.yml
similarity index 100%
rename from .erb-lint.yml
rename to .erb_lint.yml
diff --git a/Gemfile b/Gemfile
index 1030f0c677c..ee19f1969e6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -105,7 +105,7 @@ group :development, :test do
gem 'brakeman', require: false
gem 'bullet', '~> 7.0'
gem 'capybara-webmock', git: 'https://github.com/hashrocket/capybara-webmock.git', ref: 'd3f3b7c'
- gem 'erb_lint', '~> 0.5.0', require: false
+ gem 'erb_lint', '~> 0.7.0', require: false
gem 'i18n-tasks', '~> 1.0'
gem 'knapsack'
gem 'listen'
diff --git a/Gemfile.lock b/Gemfile.lock
index dda8909ff93..037d8d04e17 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -305,12 +305,12 @@ GEM
htmlentities (~> 4.3.3)
launchy (~> 2.1)
mail (~> 2.7)
- erb_lint (0.5.0)
+ erb_lint (0.7.0)
activesupport
better_html (>= 2.0.1)
parser (>= 2.7.1.4)
rainbow
- rubocop
+ rubocop (>= 1)
smart_properties
errbase (0.2.1)
erubi (1.13.0)
@@ -779,7 +779,7 @@ DEPENDENCIES
devise (~> 4.8)
dotiw (>= 4.0.1)
email_spec
- erb_lint (~> 0.5.0)
+ erb_lint (~> 0.7.0)
factory_bot_rails (>= 6.2.0)
faker
faraday (~> 2)
diff --git a/Makefile b/Makefile
index f10d272378a..482082e652d 100644
--- a/Makefile
+++ b/Makefile
@@ -63,7 +63,7 @@ check: lint test ## Runs lint tests and spec tests
lint: ## Runs all lint tests
# Ruby
- @echo "--- erb-lint ---"
+ @echo "--- erb_lint ---"
make lint_erb
@echo "--- rubocop ---"
mkdir -p tmp
@@ -112,7 +112,7 @@ audit: ## Checks packages for vulnerabilities
yarn audit --groups dependencies; test $$? -le 7
lint_erb: ## Lints ERB files
- bundle exec erblint app/views app/components
+ bundle exec erb_lint app/views app/components
lint_yaml: normalize_yaml ## Lints YAML files
(! git diff --name-only | grep "^config/.*\.yml") || (echo "Error: Run 'make normalize_yaml' to normalize YAML"; exit 1)
@@ -181,8 +181,8 @@ lint_spec_file_name:
lintfix: ## Try to automatically fix any Ruby, ERB, JavaScript, YAML, or CSS lint errors
@echo "--- rubocop fix ---"
bundle exec rubocop -a
- @echo "--- erblint fix ---"
- bundle exec erblint app/views app/components -a
+ @echo "--- erb_lint fix ---"
+ bundle exec erb_lint app/views app/components -a
@echo "--- eslint fix ---"
yarn lint --fix
@echo "--- stylelint fix ---"
diff --git a/app/controllers/concerns/ial2_profile_concern.rb b/app/controllers/concerns/ial2_profile_concern.rb
index 44951aefbe0..679660a8d59 100644
--- a/app/controllers/concerns/ial2_profile_concern.rb
+++ b/app/controllers/concerns/ial2_profile_concern.rb
@@ -23,7 +23,7 @@ def cache_profile_and_handle_errors(raw_password, profile)
cacher.save(raw_password, profile)
rescue Encryption::EncryptionError => err
if profile
- profile.deactivate(:encryption_error)
+ profile.deactivate_due_to_encryption_error
analytics.profile_encryption_invalid(error: err.message)
end
end
diff --git a/app/controllers/concerns/idv/document_capture_concern.rb b/app/controllers/concerns/idv/document_capture_concern.rb
index 790f0c64907..746657959a4 100644
--- a/app/controllers/concerns/idv/document_capture_concern.rb
+++ b/app/controllers/concerns/idv/document_capture_concern.rb
@@ -81,6 +81,38 @@ def redirect_to_correct_vendor(vendor, in_hybrid_mobile)
redirect_to correct_path
end
+ def fetch_test_verification_data
+ return unless IdentityConfig.store.socure_docv_verification_data_test_mode
+
+ docv_transaction_token_override = params.permit(:docv_token)[:docv_token]
+ return unless IdentityConfig.store.socure_docv_verification_data_test_mode_tokens.
+ include?(docv_transaction_token_override)
+
+ SocureDocvResultsJob.perform_now(
+ document_capture_session_uuid:,
+ docv_transaction_token_override:,
+ async: true,
+ )
+ end
+
+ def track_document_request_event(document_request:, document_response:, timer:)
+ document_request_body = JSON.parse(document_request.body, symbolize_names: true)[:config]
+ response_hash = document_response.to_h
+ log_extras = {
+ reference_id: response_hash[:referenceId],
+ vendor: 'Socure',
+ vendor_request_time_in_ms: timer.results['vendor_request'],
+ success: @url.present?,
+ document_type: document_request_body[:documentType],
+ docv_transaction_token: response_hash.dig(:data, :docvTransactionToken),
+ }
+ analytics_hash = log_extras.merge(analytics_arguments).
+ merge(document_request_body).except(
+ :documentType, # requested document type
+ ).merge(response_body: document_response.to_h)
+ analytics.idv_socure_document_request_submitted(**analytics_hash)
+ end
+
private
def track_document_issuing_state(user, state)
diff --git a/app/controllers/idv/account_verified_cta_visited_controller.rb b/app/controllers/idv/account_verified_cta_visited_controller.rb
index 550e65ea2c1..b058db1b588 100644
--- a/app/controllers/idv/account_verified_cta_visited_controller.rb
+++ b/app/controllers/idv/account_verified_cta_visited_controller.rb
@@ -22,7 +22,8 @@ def redirect_url
if issuer.blank?
root_url
else
- sp_return_url_resolver&.return_to_sp_url
+ sp_return_url_resolver&.post_idv_follow_up_url ||
+ sp_return_url_resolver&.return_to_sp_url
end
end
diff --git a/app/controllers/idv/by_mail/sp_follow_up_controller.rb b/app/controllers/idv/by_mail/sp_follow_up_controller.rb
new file mode 100644
index 00000000000..86381f6e2da
--- /dev/null
+++ b/app/controllers/idv/by_mail/sp_follow_up_controller.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Idv
+ module ByMail
+ class SpFollowUpController < ApplicationController
+ include Idv::AvailabilityConcern
+
+ before_action :confirm_two_factor_authenticated
+ before_action :confirm_needs_sp_follow_up
+
+ def new
+ analytics.track_event(:idv_by_mail_sp_follow_up_visited, **analytics_params)
+ @presenter = Idv::ByMail::SpFollowUpPresenter.new(current_user:)
+ end
+
+ def show
+ analytics.track_event(:idv_by_mail_sp_follow_up_submitted, **analytics_params)
+
+ sp_return_url_resolver = SpReturnUrlResolver.new(
+ service_provider: current_user.active_profile.initiating_service_provider,
+ )
+ redirect_url = sp_return_url_resolver.post_idv_follow_up_url ||
+ sp_return_url_resolver.return_to_sp_url
+ redirect_to(redirect_url, allow_other_host: true)
+ end
+
+ def cancel
+ analytics.track_event(:idv_by_mail_sp_follow_up_cancelled, **analytics_params)
+ redirect_to account_url
+ end
+
+ private
+
+ def analytics_params
+ initiating_service_provider = current_user.active_profile.initiating_service_provider
+ {
+ initiating_service_provider: initiating_service_provider.issuer,
+ }
+ end
+
+ def confirm_needs_sp_follow_up
+ return if current_user.identity_verified? &&
+ current_user.active_profile.initiating_service_provider.present? &&
+ !current_sp.present?
+ redirect_to account_url
+ end
+ end
+ end
+end
diff --git a/app/controllers/idv/enter_password_controller.rb b/app/controllers/idv/enter_password_controller.rb
index 64768ea7ec8..c91ab6044a3 100644
--- a/app/controllers/idv/enter_password_controller.rb
+++ b/app/controllers/idv/enter_password_controller.rb
@@ -127,7 +127,7 @@ def confirm_current_password
end
def init_profile
- idv_session.create_profile_from_applicant_with_password(
+ profile = idv_session.create_profile_from_applicant_with_password(
password,
is_enhanced_ipp: resolved_authn_context_result.enhanced_ipp?,
proofing_components: ProofingComponents.new(
@@ -137,12 +137,13 @@ def init_profile
user_session:,
).to_h,
)
- if idv_session.verify_by_mail?
+
+ if profile.gpo_verification_pending?
current_user.send_email_to_all_addresses(:verify_by_mail_letter_requested)
log_letter_enqueued_analytics(resend: false)
end
- if idv_session.profile.active?
+ if profile.active?
create_user_event(:account_verified)
UserAlerts::AlertUserAboutAccountVerified.call(
profile: idv_session.profile,
diff --git a/app/controllers/idv/how_to_verify_controller.rb b/app/controllers/idv/how_to_verify_controller.rb
index 233d6165d6d..5b65ae67cfb 100644
--- a/app/controllers/idv/how_to_verify_controller.rb
+++ b/app/controllers/idv/how_to_verify_controller.rb
@@ -5,6 +5,7 @@ class HowToVerifyController < ApplicationController
include Idv::AvailabilityConcern
include IdvStepConcern
include RenderConditionConcern
+ include DocAuthVendorConcern
before_action :confirm_step_allowed
before_action :set_how_to_verify_presenter
@@ -86,8 +87,16 @@ def how_to_verify_form_params
end
def set_how_to_verify_presenter
+ @mobile_required = mobile_required?
@selfie_required = idv_session.selfie_check_required
- @presenter = Idv::HowToVerifyPresenter.new(selfie_check_required: @selfie_required)
+ @presenter = Idv::HowToVerifyPresenter.new(
+ mobile_required: @mobile_required,
+ selfie_check_required: @selfie_required,
+ )
+ end
+
+ def mobile_required?
+ idv_session.selfie_check_required || doc_auth_vendor == Idp::Constants::Vendors::SOCURE
end
end
end
diff --git a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb
index 44fccfa0182..5fa8e17c763 100644
--- a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb
+++ b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb
@@ -14,6 +14,7 @@ class DocumentCaptureController < ApplicationController
before_action :check_valid_document_capture_session, except: [:update]
before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::SOCURE, true) },
only: :show
+ before_action :fetch_test_verification_data, only: [:update]
def show
Funnel::DocAuth::RegisterStep.new(document_capture_user.id, sp_session[:issuer]).
@@ -24,12 +25,16 @@ def show
redirect_url: idv_hybrid_mobile_socure_document_capture_update_url,
language: I18n.locale,
)
- document_response = document_request.fetch
+ timer = JobHelpers::Timer.new
+ document_response = timer.time('vendor_request') do
+ document_request.fetch
+ end
- @document_request = document_request
- @document_response = document_response
@url = document_response.dig(:data, :url)
+ track_document_request_event(document_request:, document_response:, timer:)
+
+ # placeholder until we get an error page for url not being present
if @url.nil?
redirect_to idv_hybrid_mobile_socure_document_capture_errors_url
return
@@ -47,9 +52,6 @@ def show
:url,
)
document_capture_session.save
- # useful for analytics
- @msg = document_response[:msg]
- @reference_id = document_response[:referenceId]
end
def update
diff --git a/app/controllers/idv/personal_key_controller.rb b/app/controllers/idv/personal_key_controller.rb
index 358b1f97fd0..8599d8291b8 100644
--- a/app/controllers/idv/personal_key_controller.rb
+++ b/app/controllers/idv/personal_key_controller.rb
@@ -72,6 +72,8 @@ def next_step
idv_please_call_url
elsif session[:sp]
sign_up_completed_url
+ elsif idv_session.address_verification_mechanism == 'gpo'
+ idv_sp_follow_up_path
else
after_sign_in_path_for(current_user)
end
diff --git a/app/controllers/idv/socure/document_capture_controller.rb b/app/controllers/idv/socure/document_capture_controller.rb
index 3f29ad659e1..8beee4f8a92 100644
--- a/app/controllers/idv/socure/document_capture_controller.rb
+++ b/app/controllers/idv/socure/document_capture_controller.rb
@@ -14,6 +14,7 @@ class DocumentCaptureController < ApplicationController
before_action :confirm_step_allowed
before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::SOCURE, false) },
only: :show
+ before_action :fetch_test_verification_data, only: [:update]
# reconsider and maybe remove these when implementing the real
# update handler
@@ -34,13 +35,15 @@ def show
redirect_url: idv_socure_document_capture_update_url,
language: I18n.locale,
)
+ timer = JobHelpers::Timer.new
+ document_response = timer.time('vendor_request') do
+ document_request.fetch
+ end
- document_response = document_request.fetch
-
- @document_request = document_request
- @document_response = document_response
@url = document_response.dig(:data, :url)
+ track_document_request_event(document_request:, document_response:, timer:)
+
# placeholder until we get an error page for url not being present
if @url.nil?
redirect_to idv_socure_document_capture_errors_url
@@ -60,10 +63,6 @@ def show
:url,
)
document_capture_session.save
-
- # useful for analytics
- @msg = document_response[:msg]
- @reference_id = document_response[:referenceId]
end
def update
diff --git a/app/controllers/users/webauthn_platform_recommended_controller.rb b/app/controllers/users/webauthn_platform_recommended_controller.rb
index 7c8395dfdcc..aa18b9b6eeb 100644
--- a/app/controllers/users/webauthn_platform_recommended_controller.rb
+++ b/app/controllers/users/webauthn_platform_recommended_controller.rb
@@ -39,7 +39,7 @@ def dismiss_redirect_path
if opted_to_add?
webauthn_setup_path(platform: true)
elsif in_account_creation_flow?
- next_setup_path
+ next_setup_path || after_mfa_setup_path
else
after_sign_in_path_for(current_user)
end
diff --git a/app/jobs/socure_docv_results_job.rb b/app/jobs/socure_docv_results_job.rb
index 25729078575..b4142a3b2b8 100644
--- a/app/jobs/socure_docv_results_job.rb
+++ b/app/jobs/socure_docv_results_job.rb
@@ -3,12 +3,13 @@
class SocureDocvResultsJob < ApplicationJob
queue_as :high_socure_docv
- attr_reader :document_capture_session_uuid, :async
+ attr_reader :document_capture_session_uuid, :async, :docv_transaction_token_override
# @param [String] document_capture_session_uuid
- def perform(document_capture_session_uuid:, async: true)
+ def perform(document_capture_session_uuid:, async: true, docv_transaction_token_override: nil)
@document_capture_session_uuid = document_capture_session_uuid
@async = async
+ @docv_transaction_token_override = docv_transaction_token_override
raise "DocumentCaptureSession not found: #{document_capture_session_uuid}" unless
document_capture_session
@@ -51,6 +52,7 @@ def log_verification_request(docv_result_response:, vendor_request_time_in_ms:)
def socure_document_verification_result
DocAuth::Socure::Requests::DocvResultRequest.new(
document_capture_session_uuid:,
+ docv_transaction_token_override:,
).fetch
end
diff --git a/app/models/profile.rb b/app/models/profile.rb
index 2052eb33eaf..e05d3c0a33f 100644
--- a/app/models/profile.rb
+++ b/app/models/profile.rb
@@ -191,6 +191,20 @@ def deactivate(reason)
update!(active: false, deactivation_reason: reason)
end
+ # Update the profile's deactivation reason to "encryption_error". As a
+ # side-effect, when the profile has an associated pending in-person
+ # enrollment it will be updated to have a status of "cancelled".
+ def deactivate_due_to_encryption_error
+ update!(
+ active: false,
+ deactivation_reason: :encryption_error,
+ )
+
+ if in_person_enrollment&.pending?
+ in_person_enrollment.cancelled!
+ end
+ end
+
def fraud_deactivation_reason?
fraud_review_pending? || fraud_rejection?
end
diff --git a/app/presenters/idv/account_verified_email_presenter.rb b/app/presenters/idv/account_verified_email_presenter.rb
index 46477f0f201..a7a5748ce40 100644
--- a/app/presenters/idv/account_verified_email_presenter.rb
+++ b/app/presenters/idv/account_verified_email_presenter.rb
@@ -16,7 +16,7 @@ def service_provider
end
def show_cta?
- !service_provider || service_provider_homepage_url.present?
+ !service_provider || service_provider_post_idv_follow_up_url.present?
end
def sign_in_url
@@ -32,11 +32,11 @@ def sign_in_url
end
def displayed_sign_in_url
- service_provider_homepage_url || root_url
+ service_provider_post_idv_follow_up_url || root_url
end
- def service_provider_homepage_url
- sp_return_url_resolver.homepage_url if service_provider
+ def service_provider_post_idv_follow_up_url
+ sp_return_url_resolver.post_idv_follow_up_url if service_provider
end
def sp_name
diff --git a/app/presenters/idv/by_mail/sp_follow_up_presenter.rb b/app/presenters/idv/by_mail/sp_follow_up_presenter.rb
new file mode 100644
index 00000000000..b63cc7b2a78
--- /dev/null
+++ b/app/presenters/idv/by_mail/sp_follow_up_presenter.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Idv
+ module ByMail
+ class SpFollowUpPresenter
+ include ActionView::Helpers::TranslationHelper
+
+ attr_reader :current_user
+
+ def initialize(current_user:)
+ @current_user = current_user
+ end
+
+ def heading
+ t(
+ 'idv.by_mail.sp_follow_up.heading',
+ service_provider: initiating_service_provider_name,
+ )
+ end
+
+ def body
+ t(
+ 'idv.by_mail.sp_follow_up.body',
+ service_provider: initiating_service_provider_name,
+ app_name: APP_NAME,
+ )
+ end
+
+ private
+
+ def initiating_service_provider_name
+ initiating_service_provider.friendly_name
+ end
+
+ def initiating_service_provider
+ @initiating_service_provider ||= current_user.active_profile.initiating_service_provider
+ end
+ end
+ end
+end
diff --git a/app/presenters/idv/how_to_verify_presenter.rb b/app/presenters/idv/how_to_verify_presenter.rb
index ed3ba1574cd..ee161ca8284 100644
--- a/app/presenters/idv/how_to_verify_presenter.rb
+++ b/app/presenters/idv/how_to_verify_presenter.rb
@@ -4,22 +4,23 @@ class Idv::HowToVerifyPresenter
include ActionView::Helpers::TagHelper
include ActionView::Helpers::TranslationHelper
- attr_reader :selfie_required
+ attr_reader :mobile_required, :selfie_required
- def initialize(selfie_check_required:)
+ def initialize(mobile_required:, selfie_check_required:)
+ @mobile_required = mobile_required
@selfie_required = selfie_check_required
end
def how_to_verify_info
- if selfie_required
- t('doc_auth.info.how_to_verify_selfie')
+ if mobile_required
+ t('doc_auth.info.how_to_verify_mobile')
else
t('doc_auth.info.how_to_verify')
end
end
def asset_url
- if selfie_required
+ if mobile_required
'idv/mobile-phone-icon.svg'
else
'idv/remote.svg'
@@ -27,7 +28,7 @@ def asset_url
end
def alt_text
- if selfie_required
+ if mobile_required
t('image_description.phone_icon')
else
t('image_description.laptop_and_phone')
@@ -35,32 +36,31 @@ def alt_text
end
def verify_online_text
- if selfie_required
- t('doc_auth.headings.verify_online_selfie')
+ if mobile_required
+ t('doc_auth.headings.verify_online_mobile')
else
t('doc_auth.headings.verify_online')
end
end
def verify_online_instruction
- if selfie_required
- t('doc_auth.info.verify_online_instruction_selfie')
- else
- t('doc_auth.info.verify_online_instruction')
- end
+ return t('doc_auth.info.verify_online_instruction_selfie') if selfie_required
+ return t('doc_auth.info.verify_online_instruction_mobile_no_selfie') if mobile_required
+
+ t('doc_auth.info.verify_online_instruction')
end
def verify_online_description
- if selfie_required
- t('doc_auth.info.verify_online_description_selfie')
+ if mobile_required
+ t('doc_auth.info.verify_online_description_mobile')
else
t('doc_auth.info.verify_online_description')
end
end
def submit
- if selfie_required
- t('forms.buttons.continue_remote_selfie')
+ if mobile_required
+ t('forms.buttons.continue_remote_mobile')
else
t('forms.buttons.continue_remote')
end
@@ -75,8 +75,8 @@ def post_office_instruction
end
def post_office_description
- if selfie_required
- t('doc_auth.info.verify_at_post_office_description_selfie')
+ if mobile_required
+ t('doc_auth.info.verify_at_post_office_description_mobile')
else
t('doc_auth.info.verify_at_post_office_description')
end
diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb
index d5f8016451d..8e867239b69 100644
--- a/app/services/analytics_events.rb
+++ b/app/services/analytics_events.rb
@@ -820,15 +820,32 @@ def gpo_confirmation_upload(
# User visited sign-in URL from the "You've been successfully verified email" CTA button
# @param issuer [String] the ServiceProvider.issuer
# @param campaign_id [String] the email campaign ID
+ # @param [Hash,nil] proofing_components User's current proofing components
+ # @option proofing_components [String,nil] 'document_check' Vendor that verified the user's ID
+ # @option proofing_components [String,nil] 'document_type' Type of ID used to verify
+ # @option proofing_components [String,nil] 'source_check' Source used to verify user's PII
+ # @option proofing_components [String,nil] 'resolution_check' Vendor for identity resolution check
+ # @option proofing_components [String,nil] 'address_check' Method used to verify user's address
+ # @option proofing_components [Boolean,nil] 'threatmetrix' Whether ThreatMetrix check was done
+ # @option proofing_components [String,nil] 'threatmetrix_review_status' TMX decision on the user
+ # @param [String,nil] active_profile_idv_level ID verification level of user's active profile.
+ # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile.
def idv_account_verified_cta_visited(
issuer:,
campaign_id:,
+ proofing_components: nil,
+ active_profile_idv_level: nil,
+ pending_profile_idv_level: nil,
**extra
)
track_event(
:idv_account_verified_cta_visited,
issuer:,
campaign_id:,
+ proofing_components:,
+ active_profile_idv_level:,
+ pending_profile_idv_level:,
+
**extra,
)
end
@@ -1032,6 +1049,36 @@ def idv_barcode_warning_retake_photos_clicked(liveness_checking_required:, **ext
)
end
+ # @param [String] initiating_service_provider The service provider the user needs to connect to
+ # The user chose not to connect their account from the SP follow-up page
+ def idv_by_mail_sp_follow_up_cancelled(initiating_service_provider:, **extra)
+ track_event(
+ :idv_by_mail_sp_follow_up_cancelled,
+ initiating_service_provider:,
+ **extra,
+ )
+ end
+
+ # @param [String] initiating_service_provider The service provider the user needs to connect to
+ # The user chose to connect their account from the SP follow-up page
+ def idv_by_mail_sp_follow_up_submitted(initiating_service_provider:, **extra)
+ track_event(
+ :idv_by_mail_sp_follow_up_submitted,
+ initiating_service_provider:,
+ **extra,
+ )
+ end
+
+ # @param [String] initiating_service_provider The service provider the user needs to connect to
+ # The user visited the SP follow-up page
+ def idv_by_mail_sp_follow_up_visited(initiating_service_provider:, **extra)
+ track_event(
+ :idv_by_mail_sp_follow_up_visited,
+ initiating_service_provider:,
+ **extra,
+ )
+ end
+
# @param [Hash] error
def idv_camera_info_error(error:, **_extra)
track_event(:idv_camera_info_error, error: error)
@@ -4682,6 +4729,76 @@ def idv_session_error_visited(
)
end
+ # @param [Boolean] success Whether form validation was successful
+ # @param [Hash] errors Errors resulting from form validation
+ # @param [String] exception any exceptions thrown during request
+ # @param [String] docv_transaction_token socure transaction token
+ # @param [String] reference_id socure interal id for transaction
+ # @param [String] language lagnuage presented to user
+ # @param [String] step current step of idv to user
+ # @param [String] analytics_id id of analytics
+ # @param [Boolean] redo_document_capture if user is redoing doc capture
+ # @param [Boolean] skip_hybrid_handoff if user is skipping handoff
+ # @param [Boolean] selfie_check_required is selfie check required
+ # @param [Boolean] opted_in_to_in_person_proofing user opts in to IPP
+ # @param [Hash] redirect hash for redirect (url and method)
+ # @param [Hash] response_body hash received from socure
+ # @param ["hybrid","standard"] flow_path Document capture user flow
+ # @param [Float] vendor_request_time_in_ms Time it took to upload images & get a response.
+ # @param [Boolean] liveness_checking_required Whether or not the selfie is required
+ # @param [Boolean] liveness_enabled Whether or not the selfie result is included in response
+ # @param [String] vendor which 2rd party we are using for doc auth
+ # @param [Hash] document_type type of socument submitted (Drivers Licenese, etc.)
+ # The request for socure verification was sent
+ def idv_socure_document_request_submitted(
+ success:,
+ redirect:,
+ liveness_checking_required:,
+ vendor_request_time_in_ms:,
+ vendor:,
+ language:,
+ step:,
+ analytics_id:,
+ response_body:,
+ redo_document_capture: nil,
+ skip_hybrid_handoff: nil,
+ selfie_check_required: nil,
+ opted_in_to_in_person_proofing: nil,
+ errors: nil,
+ exception: nil,
+ reference_id: nil,
+ liveness_enabled: nil,
+ document_type: nil,
+ docv_transaction_token: nil,
+ flow_path: nil,
+ **extra
+ )
+ track_event(
+ :idv_socure_document_request_submitted,
+ success:,
+ redirect:,
+ liveness_checking_required:,
+ vendor_request_time_in_ms:,
+ vendor:,
+ language:,
+ step:,
+ analytics_id:,
+ redo_document_capture:,
+ skip_hybrid_handoff:,
+ selfie_check_required:,
+ opted_in_to_in_person_proofing:,
+ errors:,
+ exception:,
+ reference_id:,
+ response_body:,
+ liveness_enabled:,
+ document_type:,
+ docv_transaction_token:,
+ flow_path:,
+ **extra,
+ )
+ end
+
# Socure Reason Codes were downloaded and synced against persisted codes in the database
# @param [Boolean] success Result from Socure KYC API call
# @param [Hash] errors Result from resolution proofing
diff --git a/app/services/doc_auth/socure/requests/document_request.rb b/app/services/doc_auth/socure/requests/document_request.rb
index f214f8867c6..3fa0a99b10d 100644
--- a/app/services/doc_auth/socure/requests/document_request.rb
+++ b/app/services/doc_auth/socure/requests/document_request.rb
@@ -16,13 +16,6 @@ def initialize(
@language = language
end
- private
-
- def lang(language)
- return 'zh-cn' if language == :zh
- language
- end
-
def body
redirect = {
method: 'GET',
@@ -40,6 +33,13 @@ def body
}.to_json
end
+ private
+
+ def lang(language)
+ return 'zh-cn' if language == :zh
+ language
+ end
+
def handle_http_response(http_response)
JSON.parse(http_response.body, symbolize_names: true)
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 7f4bc26d116..8a3662343a6 100644
--- a/app/services/doc_auth/socure/requests/docv_result_request.rb
+++ b/app/services/doc_auth/socure/requests/docv_result_request.rb
@@ -6,8 +6,13 @@ module Requests
class DocvResultRequest < DocAuth::Socure::Request
attr_reader :document_capture_session_uuid, :biometric_comparison_required
- def initialize(document_capture_session_uuid:, biometric_comparison_required: false)
+ def initialize(
+ document_capture_session_uuid:,
+ docv_transaction_token_override: nil,
+ biometric_comparison_required: false
+ )
@document_capture_session_uuid = document_capture_session_uuid
+ @docv_transaction_token_override = docv_transaction_token_override
@biometric_comparison_required = biometric_comparison_required
end
@@ -16,7 +21,7 @@ def initialize(document_capture_session_uuid:, biometric_comparison_required: fa
def body
{
modules: ['documentverification'],
- docvTransactionToken: document_capture_session.socure_docv_transaction_token,
+ docvTransactionToken: docv_transaction_token,
}.to_json
end
@@ -60,6 +65,16 @@ def endpoint
def metric_name
'socure_id_plus_document_verification'
end
+
+ def docv_transaction_token
+ if IdentityConfig.store.socure_docv_verification_data_test_mode &&
+ IdentityConfig.store.socure_docv_verification_data_test_mode_tokens.
+ include?(@docv_transaction_token_override)
+ return @docv_transaction_token_override
+ end
+
+ document_capture_session.socure_docv_transaction_token
+ end
end
end
end
diff --git a/app/services/idv/analytics_events_enhancer.rb b/app/services/idv/analytics_events_enhancer.rb
index 810e933438d..2b713d57d8d 100644
--- a/app/services/idv/analytics_events_enhancer.rb
+++ b/app/services/idv/analytics_events_enhancer.rb
@@ -31,7 +31,6 @@ module AnalyticsEventsEnhancer
idv_doc_auth_ssn_visited
idv_doc_auth_submitted_image_upload_form
idv_doc_auth_submitted_image_upload_vendor
- idv_socure_verification_data_requested
idv_doc_auth_submitted_pii_validation
idv_doc_auth_verify_proofing_results
idv_doc_auth_verify_submitted
@@ -97,6 +96,8 @@ module AnalyticsEventsEnhancer
idv_sdk_selfie_image_capture_opened
idv_selfie_image_added
idv_session_error_visited
+ idv_socure_document_request_submitted
+ idv_socure_verification_data_requested
idv_threatmetrix_response_body
idv_usps_auth_token_refresh_job_completed
idv_usps_auth_token_refresh_job_network_error
diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb
index 071ce403a80..f7f48f83add 100644
--- a/app/services/idv/session.rb
+++ b/app/services/idv/session.rb
@@ -105,6 +105,7 @@ def respond_to_missing?(method_sym, include_private)
VALID_SESSION_ATTRIBUTES.include?(attr_name_sym) || super
end
+ # @return [Profile]
def create_profile_from_applicant_with_password(
user_password, is_enhanced_ipp:, proofing_components:
)
@@ -141,6 +142,8 @@ def create_profile_from_applicant_with_password(
if profile.gpo_verification_pending?
create_gpo_entry(profile_maker.pii_attributes, profile)
end
+
+ profile
end
def opt_in_param
diff --git a/app/services/proofing/lexis_nexis/ddp/response_redacter.rb b/app/services/proofing/lexis_nexis/ddp/response_redacter.rb
index 75dda7dc8cd..721a01738a4 100644
--- a/app/services/proofing/lexis_nexis/ddp/response_redacter.rb
+++ b/app/services/proofing/lexis_nexis/ddp/response_redacter.rb
@@ -152,7 +152,6 @@ class ResponseRedacter
national_id_worst_score
org_id
policy
- policy_details_api
policy_engine_version
policy_score
page_time_on
diff --git a/app/services/saml_request_validator.rb b/app/services/saml_request_validator.rb
index d055f00c766..3364854d94e 100644
--- a/app/services/saml_request_validator.rb
+++ b/app/services/saml_request_validator.rb
@@ -3,8 +3,9 @@
class SamlRequestValidator
include ActiveModel::Model
- validate :cert_exists
+ validate :request_cert_exists
validate :authorized_service_provider
+ validate :registered_cert_exists
validate :authorized_authn_context
validate :parsable_vtr
validate :authorized_email_nameid_format
@@ -72,7 +73,18 @@ def parsable_vtr
end
end
- def cert_exists
+ def registered_cert_exists
+ # if there is no service provider, this error has already been added
+ return if service_provider.blank?
+ return if service_provider.certs.present?
+
+ errors.add(
+ :service_provider, :no_cert_registered,
+ type: :no_cert_registered
+ )
+ end
+
+ def request_cert_exists
if @blank_cert
errors.add(:service_provider, :blank_cert_element_req, type: :blank_cert_element_req)
end
diff --git a/app/services/sp_return_url_resolver.rb b/app/services/sp_return_url_resolver.rb
index dbed6e0f4b4..0e2f2e3bfd8 100644
--- a/app/services/sp_return_url_resolver.rb
+++ b/app/services/sp_return_url_resolver.rb
@@ -24,6 +24,10 @@ def homepage_url
service_provider.return_to_sp_url
end
+ def post_idv_follow_up_url
+ service_provider.post_idv_follow_up_url || homepage_url
+ end
+
private
def inferred_redirect_url
diff --git a/app/views/idv/by_mail/sp_follow_up/new.html.erb b/app/views/idv/by_mail/sp_follow_up/new.html.erb
new file mode 100644
index 00000000000..972f905773c
--- /dev/null
+++ b/app/views/idv/by_mail/sp_follow_up/new.html.erb
@@ -0,0 +1,24 @@
+<% self.title = @presenter.heading %>
+
+
+ <%= image_tag(asset_url('user-access.svg'), width: '280', height: '91', alt: '', aria: { hidden: true }) %>
+
<%= @presenter.heading %>
+
+
+<%= @presenter.body %>
+
+
+ <%= render ButtonComponent.new(
+ url: idv_sp_follow_up_connect_path,
+ big: true,
+ wide: true,
+ ).with_content(t('idv.by_mail.sp_follow_up.connect_account')) %>
+
+
+ <%= render ButtonComponent.new(
+ url: idv_sp_follow_up_cancel_path,
+ big: true,
+ wide: true,
+ outline: true,
+ ).with_content(t('idv.by_mail.sp_follow_up.go_to_account')) %>
+
diff --git a/app/views/idv/how_to_verify/show.html.erb b/app/views/idv/how_to_verify/show.html.erb
index bb19ea1e96a..4c1ee1f7fa8 100644
--- a/app/views/idv/how_to_verify/show.html.erb
+++ b/app/views/idv/how_to_verify/show.html.erb
@@ -50,7 +50,7 @@
<%= f.label(
:selection_remote,
) do %>
- <% if @selfie_required %>
+ <% if @mobile_required %>
<%= t('doc_auth.tips.mobile_phone_required') %>
diff --git a/app/views/users/webauthn_platform_recommended/new.html.erb b/app/views/users/webauthn_platform_recommended/new.html.erb
index a0e357ff886..534df7a338e 100644
--- a/app/views/users/webauthn_platform_recommended/new.html.erb
+++ b/app/views/users/webauthn_platform_recommended/new.html.erb
@@ -23,18 +23,19 @@
- <%= render ButtonComponent.new(
+ <%= render SubmitButtonComponent.new(
url: webauthn_platform_recommended_url,
method: :post,
params: { add_method: true },
- big: true,
full_width: true,
class: 'margin-bottom-2',
).with_content(t('webauthn_platform_recommended.cta')) %>
- <%= render ButtonComponent.new(
+ <%= render SubmitButtonComponent.new(
url: webauthn_platform_recommended_url,
method: :post,
unstyled: true,
+ big: false,
+ wide: false,
).with_content(t('webauthn_platform_recommended.skip')) %>
diff --git a/config/application.yml.default b/config/application.yml.default
index 86e01893ec2..549a443c771 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -385,6 +385,8 @@ sign_in_user_id_per_ip_max_attempts: 50
skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:dev", "urn:gov:gsa:SAML:2.0.profiles:sp:sso:int"]'
socure_docv_document_request_endpoint: ''
socure_docv_enabled: false
+socure_docv_verification_data_test_mode: false
+socure_docv_verification_data_test_mode_tokens: '[]'
socure_docv_webhook_secret_key: ''
socure_docv_webhook_secret_key_queue: '[]'
socure_idplus_api_key: ''
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 7593fbd2ff2..dddee2c7af2 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -616,7 +616,7 @@ doc_auth.headings.upload_from_phone: Use your phone to take photos
doc_auth.headings.verify_at_post_office: Verify your identity at a Post Office
doc_auth.headings.verify_identity: Verify your identity
doc_auth.headings.verify_online: Verify your identity online
-doc_auth.headings.verify_online_selfie: Verify your identity online using your phone
+doc_auth.headings.verify_online_mobile: Verify your identity online using your phone
doc_auth.headings.verify_with_phone: Verify your identity using your phone
doc_auth.headings.welcome: Let’s verify your identity for %{sp_name}
doc_auth.hybrid_flow_warning.explanation_html: You’re using %{app_name} to verify your identity for access to %{service_provider_name} and its services.
@@ -637,7 +637,7 @@ doc_auth.info.exit.without_sp: Exit identity verification and go to your account
doc_auth.info.getting_started_html: '%{sp_name} needs to make sure you are you — not someone pretending to be you. %{link_html}'
doc_auth.info.getting_started_learn_more: Learn more about verifying your identity
doc_auth.info.how_to_verify: You have the option to verify your identity online, or in person at a participating Post Office.
-doc_auth.info.how_to_verify_selfie: You have the option to verify your identity online with your phone, or in person at a participating Post Office.
+doc_auth.info.how_to_verify_mobile: You have the option to verify your identity online with your phone, or in person at a participating Post Office.
doc_auth.info.how_to_verify_troubleshooting_options_header: Want to learn more about how to verify your identity?
doc_auth.info.hybrid_handoff: We’ll collect information about you by reading your state‑issued ID.
doc_auth.info.hybrid_handoff_ipp_html: 'Don’t have a mobile phone? You can verify your identity at a United States Post Office instead.'
@@ -674,14 +674,15 @@ doc_auth.info.tag: Recommended
doc_auth.info.upload_from_computer: Don’t have a phone? Upload photos of your ID from this computer.
doc_auth.info.upload_from_phone: You won’t have to sign in again, and you’ll switch back to this computer after you take photos. Your mobile phone must have a camera and a web browser.
doc_auth.info.verify_at_post_office_description: This option is better if you don’t have a phone to take photos of your ID.
-doc_auth.info.verify_at_post_office_description_selfie: Choose this option if you don’t have a phone to take pictures.
+doc_auth.info.verify_at_post_office_description_mobile: Choose this option if you don’t have a phone to take pictures.
doc_auth.info.verify_at_post_office_instruction: You’ll enter your ID information online, and verify your identity in person at a participating Post Office.
doc_auth.info.verify_at_post_office_instruction_selfie: You’ll enter your ID information online, and verify your identity in person at a participating Post Office.
doc_auth.info.verify_at_post_office_link_text: Learn more about verifying in person
doc_auth.info.verify_identity: We’ll ask for your ID, phone number, and other personal information to verify your identity against public records.
doc_auth.info.verify_online_description: This option is better if you have a phone to take photos of your ID.
-doc_auth.info.verify_online_description_selfie: Choose this option if you have a phone to take pictures.
+doc_auth.info.verify_online_description_mobile: Choose this option if you have a phone to take pictures.
doc_auth.info.verify_online_instruction: You’ll take photos of your ID to verify your identity fully online. Most users finish this process in one sitting.
+doc_auth.info.verify_online_instruction_mobile_no_selfie: You’ll take photos of your ID using a phone. Most users finish this process in one sitting.
doc_auth.info.verify_online_instruction_selfie: You’ll take photos of your ID and a photo of yourself using a phone. Most users finish this process in one sitting.
doc_auth.info.verify_online_link_text: Learn more about verifying online
doc_auth.info.you_entered: 'You entered:'
@@ -765,6 +766,7 @@ errors.messages.invalid_recaptcha_token: We’re sorry, but your computer or net
errors.messages.invalid_sms_number: The phone number entered doesn’t support text messaging. Try the Phone call option.
errors.messages.invalid_voice_number: Invalid phone number. Check that you’ve entered the correct country code or area code.
errors.messages.missing_field: Please fill in this field.
+errors.messages.no_cert_registered: Your service provider does not have a certificate registered.
errors.messages.no_pending_profile: No profile is waiting for verification
errors.messages.not_a_number: is not a number
errors.messages.otp_format: Enter your entire one-time code without spaces or special characters
@@ -862,7 +864,7 @@ forms.buttons.confirm: Confirm
forms.buttons.continue: Continue
forms.buttons.continue_ipp: Continue in person
forms.buttons.continue_remote: Continue online
-forms.buttons.continue_remote_selfie: Continue on your phone
+forms.buttons.continue_remote_mobile: Continue on your phone
forms.buttons.delete: Delete
forms.buttons.disable: Delete
forms.buttons.edit: Edit
@@ -1011,6 +1013,10 @@ idv.buttons.change_ssn_label: Update Social Security number
idv.buttons.change_state_id_label: Update state ID
idv.buttons.continue_plain: Continue
idv.buttons.mail.send: Request a letter
+idv.by_mail.sp_follow_up.body: Sign back in to %{service_provider} to connect your verified %{app_name} account and access services.
+idv.by_mail.sp_follow_up.connect_account: Connect your account
+idv.by_mail.sp_follow_up.go_to_account: Go to account
+idv.by_mail.sp_follow_up.heading: Connect to %{service_provider}
idv.cancel.actions.account_page: Go to account page
idv.cancel.actions.exit: Exit %{app_name}
idv.cancel.actions.keep_going: No, keep going
diff --git a/config/locales/es.yml b/config/locales/es.yml
index baffc5ec289..2b9ec65d8af 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -627,7 +627,7 @@ doc_auth.headings.upload_from_phone: Utilice su teléfono para tomar fotos
doc_auth.headings.verify_at_post_office: Verifique su identidad en una oficina de correos
doc_auth.headings.verify_identity: Verifique su identidad
doc_auth.headings.verify_online: Verifique su identidad en línea
-doc_auth.headings.verify_online_selfie: Verifique su identidad en línea con su teléfono
+doc_auth.headings.verify_online_mobile: Verifique su identidad en línea con su teléfono
doc_auth.headings.verify_with_phone: Verifique su identidad con su teléfono
doc_auth.headings.welcome: Verifiquemos su identidad para %{sp_name}
doc_auth.hybrid_flow_warning.explanation_html: Está utilizando %{app_name} para verificar su identidad y acceder a %{service_provider_name} y sus servicios.
@@ -648,7 +648,7 @@ doc_auth.info.exit.without_sp: Salga de la verificación de identidad y vaya a l
doc_auth.info.getting_started_html: '%{sp_name} necesita asegurarse de que se trata de usted y no de alguien que se hace pasar por usted. %{link_html}'
doc_auth.info.getting_started_learn_more: Obtenga más información sobre la verificación de su identidad
doc_auth.info.how_to_verify: Tiene la opción de verificar su identidad en línea, o en persona en una oficina de correos participante.
-doc_auth.info.how_to_verify_selfie: Tiene la opción de verificar su identidad en línea con su teléfono, o en persona en una oficina de correos participante.
+doc_auth.info.how_to_verify_mobile: Tiene la opción de verificar su identidad en línea con su teléfono, o en persona en una oficina de correos participante.
doc_auth.info.how_to_verify_troubleshooting_options_header: ¿Desea obtener más información sobre cómo verificar su identidad?
doc_auth.info.hybrid_handoff: Recopilaremos información sobre usted leyendo su identificación emitida por el estado.
doc_auth.info.hybrid_handoff_ipp_html: '¿No tiene un teléfono móvil? Puede verificar su identidad en una oficina de correos de los Estados Unidos.'
@@ -685,14 +685,15 @@ doc_auth.info.tag: Recomendado
doc_auth.info.upload_from_computer: '¿No tiene un teléfono? Cargue fotos de su identificación desde esta computadora.'
doc_auth.info.upload_from_phone: No tendrá que volver a iniciar sesión y volverá a esta computadora después de tomar las fotos. Su teléfono móvil debe tener una cámara y un navegador web.
doc_auth.info.verify_at_post_office_description: Esta opción es mejor si no tiene un teléfono para tomar fotografías de su identificación.
-doc_auth.info.verify_at_post_office_description_selfie: Elija esta opción si no tiene un teléfono para tomar fotografías.
+doc_auth.info.verify_at_post_office_description_mobile: Elija esta opción si no tiene un teléfono para tomar fotografías.
doc_auth.info.verify_at_post_office_instruction: Ingresará la información de su identificación en línea, y verificará su identidad en persona en una oficina de correos participante.
doc_auth.info.verify_at_post_office_instruction_selfie: Ingresará la información de su identificación en línea, y verificará su identidad en persona en una oficina de correos participante.
doc_auth.info.verify_at_post_office_link_text: Obtenga más información sobre la verificación en persona
doc_auth.info.verify_identity: Le pediremos su identificación, número de teléfono y otros datos personales para verificar su identidad comparándola con los registros públicos.
doc_auth.info.verify_online_description: Esta opción es mejor si tiene un teléfono para tomar fotografías de su identificación.
-doc_auth.info.verify_online_description_selfie: Elija esta opción si tiene un teléfono para tomar fotografías.
+doc_auth.info.verify_online_description_mobile: Elija esta opción si tiene un teléfono para tomar fotografías.
doc_auth.info.verify_online_instruction: Tomará fotografías de su identificación para verificar su identidad por completo en línea. La mayoría de los usuarios logran terminar este proceso en una sola sesión.
+doc_auth.info.verify_online_instruction_mobile_no_selfie: Tomará fotografías de su identificación con un teléfono. La mayoría de los usuarios logran terminar este proceso en una sola sesión.
doc_auth.info.verify_online_instruction_selfie: Tomará con un teléfono fotos de su identificación y una foto de usted. La mayoría de los usuarios logran terminar este proceso en una sola sesión.
doc_auth.info.verify_online_link_text: Obtenga más información sobre la verificación en línea
doc_auth.info.you_entered: 'Usted ingresó:'
@@ -776,6 +777,7 @@ errors.messages.invalid_recaptcha_token: Lo sentimos, pero es posible que tu com
errors.messages.invalid_sms_number: El número de teléfono ingresado no admite mensajes de texto. Intente la opción de llamada telefónica.
errors.messages.invalid_voice_number: Número de teléfono no válido. Verifique haber ingresado el código de país o de área correcto.
errors.messages.missing_field: Llene este campo.
+errors.messages.no_cert_registered: No podemos detectar un certificado en su solicitud.
errors.messages.no_pending_profile: No hay ningún perfil en espera de verificación
errors.messages.not_a_number: no es un número
errors.messages.otp_format: Ingrese su código de un solo uso completo, sin espacios ni caracteres especiales.
@@ -873,7 +875,7 @@ forms.buttons.confirm: Confirmar
forms.buttons.continue: Continuar
forms.buttons.continue_ipp: Continúe en persona
forms.buttons.continue_remote: Continúe en línea
-forms.buttons.continue_remote_selfie: Continúe en su teléfono
+forms.buttons.continue_remote_mobile: Continúe en su teléfono
forms.buttons.delete: Eliminar
forms.buttons.disable: Eliminar
forms.buttons.edit: Editar
@@ -1022,6 +1024,10 @@ idv.buttons.change_ssn_label: Actualizar número de Seguro Social
idv.buttons.change_state_id_label: Actualizar identificación estatal
idv.buttons.continue_plain: Continuar
idv.buttons.mail.send: Solicitar una carta
+idv.by_mail.sp_follow_up.body: Vuelva a iniciar sesión en %{service_provider} para conectar su cuenta verificada de %{app_name} y acceder a los servicios.
+idv.by_mail.sp_follow_up.connect_account: Conecte su cuenta
+idv.by_mail.sp_follow_up.go_to_account: Ir a la cuenta
+idv.by_mail.sp_follow_up.heading: Conéctese a %{service_provider}
idv.cancel.actions.account_page: Ir a la página de la cuenta
idv.cancel.actions.exit: Salir de %{app_name}
idv.cancel.actions.keep_going: No, continuar
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index dc2ae99a5ed..1e7ef47f685 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -616,7 +616,7 @@ doc_auth.headings.upload_from_phone: Utiliser votre téléphone pour prendre des
doc_auth.headings.verify_at_post_office: Confirmer votre identité en personne dans un bureau de poste
doc_auth.headings.verify_identity: Confirmer votre identité
doc_auth.headings.verify_online: Confirmer votre identité en ligne
-doc_auth.headings.verify_online_selfie: Confirmer votre identité en ligne à l’aide de votre téléphone
+doc_auth.headings.verify_online_mobile: Confirmer votre identité en ligne à l’aide de votre téléphone
doc_auth.headings.verify_with_phone: Confirmer votre identité à l’aide de votre téléphone
doc_auth.headings.welcome: Vérifions votre identité auprès de %{sp_name}
doc_auth.hybrid_flow_warning.explanation_html: Vous utilisez %{app_name} pour confirmer votre identité et accéder à %{service_provider_name} et à ses services.
@@ -637,7 +637,7 @@ doc_auth.info.exit.without_sp: Quitter la vérification d’identité et accéde
doc_auth.info.getting_started_html: '%{sp_name} doit s’assurer qu’il s’agit bien de vous et non de quelqu’un qui se fait passer pour vous. %{link_html}'
doc_auth.info.getting_started_learn_more: En savoir plus sur la vérification de votre identité
doc_auth.info.how_to_verify: Vous pouvez confirmer votre identité en ligne ou en personne dans un bureau de poste participant.
-doc_auth.info.how_to_verify_selfie: Vous avez la possibilité de confirmer votre identité en ligne avec votre téléphone ou en personne dans un bureau de poste participant.
+doc_auth.info.how_to_verify_mobile: Vous avez la possibilité de confirmer votre identité en ligne avec votre téléphone ou en personne dans un bureau de poste participant.
doc_auth.info.how_to_verify_troubleshooting_options_header: Vous voulez en savoir plus sur la façon de confirmer votre identité?
doc_auth.info.hybrid_handoff: Nous recueillerons des informations vous concernant en lisant votre pièce d’identité délivrée par un État.
doc_auth.info.hybrid_handoff_ipp_html: 'Vous n’avez pas de téléphone portable? Vous pouvez confirmer votre identité dans un bureau de poste américain participant.'
@@ -674,14 +674,15 @@ doc_auth.info.tag: Recommandation
doc_auth.info.upload_from_computer: Vous n’avez pas de téléphone ? Téléchargez les photos de votre pièce d’identité depuis cet ordinateur.
doc_auth.info.upload_from_phone: Vous n’aurez pas à vous reconnecter. Vous reviendrez sur cet ordinateur après avoir pris des photos. Votre téléphone portable doit être équipé d’un appareil photo et d’un navigateur web.
doc_auth.info.verify_at_post_office_description: Cette option est préférable si vous n’avez pas de téléphone permettant de prendre des photos de votre pièce d’identité.
-doc_auth.info.verify_at_post_office_description_selfie: Choisissez cette option si vous ne disposez pas d’un téléphone permettant de prendre des photos.
+doc_auth.info.verify_at_post_office_description_mobile: Choisissez cette option si vous ne disposez pas d’un téléphone permettant de prendre des photos.
doc_auth.info.verify_at_post_office_instruction: Vous saisirez vos données d’identification en ligne et confirmerez votre identité en personne dans un bureau de poste participant.
doc_auth.info.verify_at_post_office_instruction_selfie: Vous saisirez vos données d’identification en ligne et confirmez votre identité en personne dans un bureau de poste participant.
doc_auth.info.verify_at_post_office_link_text: En savoir plus sur la vérification en personne
doc_auth.info.verify_identity: Nous vous demanderons votre pièce d’identité, numéro de téléphone et d’autres renseignements personnels afin de confirmer votre identité par rapport aux registres publics.
doc_auth.info.verify_online_description: Cette option est préférable si vous disposez d’un téléphone permettant de prendre des photos de votre pièce d’identité.
-doc_auth.info.verify_online_description_selfie: Choisissez cette option si vous disposez d’un téléphone permettant de prendre des photos.
+doc_auth.info.verify_online_description_mobile: Choisissez cette option si vous disposez d’un téléphone permettant de prendre des photos.
doc_auth.info.verify_online_instruction: Vous prendrez des photos de votre pièce d’identité pour confirmer votre identité entièrement en ligne. La plupart des utilisateurs y parviennent en une seule prise.
+doc_auth.info.verify_online_instruction_mobile_no_selfie: Vous prendrez des photos de votre pièce d’identité à l’aide d’un téléphone. La plupart des utilisateurs effectuent l’ensemble du processus en une seule fois.
doc_auth.info.verify_online_instruction_selfie: Vous prendrez des photos de votre pièce d’identité et une photo de vous-même avec un téléphone. La plupart des utilisateurs y parviennent en une seule prise.
doc_auth.info.verify_online_link_text: En savoir plus sur la vérification en ligne
doc_auth.info.you_entered: 'Vous avez saisi :'
@@ -765,6 +766,7 @@ errors.messages.invalid_recaptcha_token: Désolé, il est possible que votre ord
errors.messages.invalid_sms_number: Le numéro de téléphone saisi ne prend pas en charge les messages texte. Veuillez essayer l’option d’appel téléphonique.
errors.messages.invalid_voice_number: Numéro de téléphone non valide. Vérifiez que vous avez entré le bon indicatif international ou régional.
errors.messages.missing_field: Veuillez remplir ce champ.
+errors.messages.no_cert_registered: Nous ne pouvons pas détecter un certificat sur votre demande.
errors.messages.no_pending_profile: Aucun profil en attente de vérification
errors.messages.not_a_number: n’est pas un chiffre
errors.messages.otp_format: Saisissez l’intégralité de votre code à usage unique sans espaces ni caractères spéciaux
@@ -862,7 +864,7 @@ forms.buttons.confirm: Confirmer
forms.buttons.continue: Suite
forms.buttons.continue_ipp: Continuer en personne
forms.buttons.continue_remote: Continuer en ligne
-forms.buttons.continue_remote_selfie: Continuer sur votre téléphone
+forms.buttons.continue_remote_mobile: Continuer sur votre téléphone
forms.buttons.delete: Supprimer
forms.buttons.disable: Supprimer
forms.buttons.edit: Modifier
@@ -1011,6 +1013,10 @@ idv.buttons.change_ssn_label: Mettre à jour votre numéro de sécurité sociale
idv.buttons.change_state_id_label: Mettre à jour votre pièce d’identité
idv.buttons.continue_plain: Suite
idv.buttons.mail.send: Demander une lettre
+idv.by_mail.sp_follow_up.body: Connectez-vous à nouveau à %{service_provider} pour y associer votre compte %{app_name} vérifié et accéder aux services de cet organisme.
+idv.by_mail.sp_follow_up.connect_account: Associer votre compte
+idv.by_mail.sp_follow_up.go_to_account: Aller sur le compte
+idv.by_mail.sp_follow_up.heading: Vous connecter à %{service_provider}
idv.cancel.actions.account_page: Aller à la page de votre compte
idv.cancel.actions.exit: Quitter %{app_name}
idv.cancel.actions.keep_going: Non, continuer
diff --git a/config/locales/zh.yml b/config/locales/zh.yml
index ea75d8f839a..d4727ee6bf5 100644
--- a/config/locales/zh.yml
+++ b/config/locales/zh.yml
@@ -627,7 +627,7 @@ doc_auth.headings.upload_from_phone: 使用手机拍照
doc_auth.headings.verify_at_post_office: 去邮局验证身份
doc_auth.headings.verify_identity: 验证你的身份
doc_auth.headings.verify_online: 在网上验证身份
-doc_auth.headings.verify_online_selfie: 使用手机在网上验证你的身份
+doc_auth.headings.verify_online_mobile: 使用手机在网上验证你的身份
doc_auth.headings.verify_with_phone: 使用你的电话来验证你的身份
doc_auth.headings.welcome: 现在我们来为%{sp_name}验证你的身份
doc_auth.hybrid_flow_warning.explanation_html: 你在使用 %{app_name} 验证身份以访问 %{service_provider_name} 及其服务。
@@ -648,7 +648,7 @@ doc_auth.info.exit.without_sp: 退出身份验证并到你的账户页面
doc_auth.info.getting_started_html: '%{sp_name} 需要确保你是你,而不是别人冒充你。 了解更多有关验证你身份的信息 %{link_html}'
doc_auth.info.getting_started_learn_more: 了解有关验证你身份的更多信息
doc_auth.info.how_to_verify: 你可以选择在网上验证身份或者到参与本项目的邮局亲身验证身份。
-doc_auth.info.how_to_verify_selfie: 你可以选择用你的手机在网上验证身份或者到参与本项目的邮局亲身验证身份。
+doc_auth.info.how_to_verify_mobile: 你可以选择用你的手机在网上验证身份或者到参与本项目的邮局亲身验证身份。
doc_auth.info.how_to_verify_troubleshooting_options_header: 想对验证身份获得更多了解吗?
doc_auth.info.hybrid_handoff: 我们将通过读取州政府颁发给你的身份证件来收集你的信息。
doc_auth.info.hybrid_handoff_ipp_html: '没有手机? 你可以去一个美国邮局验证身份。'
@@ -685,14 +685,15 @@ doc_auth.info.tag: 建议
doc_auth.info.upload_from_computer: 没有手机?从该电脑上传你身份证件的照片。
doc_auth.info.upload_from_phone: 你不必重新登录,而且拍完照后可以转回到该电脑。你的手机必须带有相机和网络浏览器。
doc_auth.info.verify_at_post_office_description: 如果你没有手机拍身份证件照片,这一选项更好。
-doc_auth.info.verify_at_post_office_description_selfie: 如果你没有手机可以拍照,请选择该选项
+doc_auth.info.verify_at_post_office_description_mobile: 如果你没有手机可以拍照,请选择该选项
doc_auth.info.verify_at_post_office_instruction: 你在网上输入身份证件信息,然后到一个参与本项目的邮局去亲身验证身份。
doc_auth.info.verify_at_post_office_instruction_selfie: 你在网上输入身份证件信息,然后到一个参与本项目的邮局去亲身验证身份。
doc_auth.info.verify_at_post_office_link_text: 对亲身验证获得更多了解
doc_auth.info.verify_identity: 我们会要求获得你的身份识别、电话号码和其他个人信息并通过与公共记录核对来验证你的身份。
doc_auth.info.verify_online_description: 如果你没有移动设备或者无法轻松拍身份证件照片,这一选项更好。
-doc_auth.info.verify_online_description_selfie: 如果你有手机可以拍照,请选择该选项。
+doc_auth.info.verify_online_description_mobile: 如果你有手机可以拍照,请选择该选项。
doc_auth.info.verify_online_instruction: 你将拍身份证件的照片来完全在网上验证身份。大多数用户都能轻松完成这样流程。
+doc_auth.info.verify_online_instruction_mobile_no_selfie: 你将使用手机拍摄你的身份证件照片。大多数用户一次就能完成这个流程。
doc_auth.info.verify_online_instruction_selfie: 你将用手机拍摄身份证件和本人的照片。大多数用户都能轻松完成这一流程。
doc_auth.info.verify_online_link_text: 对网上验证获得更多了解
doc_auth.info.you_entered: 你输入了:
@@ -776,6 +777,7 @@ errors.messages.invalid_recaptcha_token: 你必须完成预防滥发邮件测验
errors.messages.invalid_sms_number: 输入的电话号码不支持短信。尝试接听电话选项。
errors.messages.invalid_voice_number: 电话号码有误。检查一下你是否输入了正确的国家代码或区域代码。
errors.messages.missing_field: 请填写这一字段。
+errors.messages.no_cert_registered: 我们在你的请求中探查不到证书。
errors.messages.no_pending_profile: 没有等待验证的用户资料
errors.messages.not_a_number: 不是数字
errors.messages.otp_format: 输入你完整的一次性代码(没有空白或特殊字符)
@@ -873,7 +875,7 @@ forms.buttons.confirm: 确认
forms.buttons.continue: 继续
forms.buttons.continue_ipp: 继续亲身验证
forms.buttons.continue_remote: 继续在网上验证
-forms.buttons.continue_remote_selfie: 在手机上继续
+forms.buttons.continue_remote_mobile: 在手机上继续
forms.buttons.delete: 删除
forms.buttons.disable: 删除
forms.buttons.edit: 编辑
@@ -1024,6 +1026,10 @@ idv.buttons.change_ssn_label: 更新社会保障号码
idv.buttons.change_state_id_label: 更新州颁发的身份证件
idv.buttons.continue_plain: 继续
idv.buttons.mail.send: 要求发一封信
+idv.by_mail.sp_follow_up.body: 重新登录 %{service_provider}以连接你验证过的%{app_name}账户并获得服务。
+idv.by_mail.sp_follow_up.connect_account: 连接你的账户
+idv.by_mail.sp_follow_up.go_to_account: 前往账户
+idv.by_mail.sp_follow_up.heading: 连接 %{service_provider}
idv.cancel.actions.account_page: 到账户页面
idv.cancel.actions.exit: 退出 %{app_name}
idv.cancel.actions.keep_going: 不是,继续
diff --git a/config/routes.rb b/config/routes.rb
index f30ff1f5936..78c4010b282 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -451,6 +451,9 @@
end
get '/by_mail/letter_enqueued' => 'by_mail/letter_enqueued#show', as: :letter_enqueued
+ get '/by_mail/sp_follow_up' => 'by_mail/sp_follow_up#new', as: :sp_follow_up
+ get '/by_mail/sp_follow_up/connect' => 'by_mail/sp_follow_up#show', as: :sp_follow_up_connect
+ get '/by_mail/sp_follow_up/cancel' => 'by_mail/sp_follow_up#cancel', as: :sp_follow_up_cancel
# We re-mapped `/verify/by_mail` to `/verify/by_mail/enter_code`. However, we sent emails to
# users with a link to `/verify/by_mail?did_not_receive_letter=1`. We need to continue
diff --git a/config/service_providers.localdev.yml b/config/service_providers.localdev.yml
index 9b905e8ad0f..2a69dd342e1 100644
--- a/config/service_providers.localdev.yml
+++ b/config/service_providers.localdev.yml
@@ -217,6 +217,7 @@ test:
assertion_consumer_logout_service_url: ''
ial: 2
allow_prompt_login: true
+ post_idv_follow_up_url: http://localhost/post_idv_follow_up_url
'urn:gov:gsa:openidconnect:sp:server_ial1':
agency_id: 2
diff --git a/db/primary_migrate/20241203163014_add_post_idv_follow_up_url_to_service_provider.rb b/db/primary_migrate/20241203163014_add_post_idv_follow_up_url_to_service_provider.rb
new file mode 100644
index 00000000000..44440e44430
--- /dev/null
+++ b/db/primary_migrate/20241203163014_add_post_idv_follow_up_url_to_service_provider.rb
@@ -0,0 +1,5 @@
+class AddPostIdvFollowUpUrlToServiceProvider < ActiveRecord::Migration[7.2]
+ def change
+ add_column :service_providers, :post_idv_follow_up_url, :string, comment: 'sensitive=false'
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0c4f93cf8d4..135f2b0231c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.2].define(version: 2024_11_15_215510) do
+ActiveRecord::Schema[7.2].define(version: 2024_12_03_163014) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "pg_stat_statements"
@@ -551,6 +551,7 @@
t.boolean "use_legacy_name_id_behavior", default: false, comment: "sensitive=false"
t.boolean "irs_attempts_api_enabled", comment: "sensitive=false"
t.boolean "in_person_proofing_enabled", default: false, comment: "sensitive=false"
+ t.string "post_idv_follow_up_url", comment: "sensitive=false"
t.index ["issuer"], name: "index_service_providers_on_issuer", unique: true
end
diff --git a/lib/identity_config.rb b/lib/identity_config.rb
index b66644e7792..cd90324d7ed 100644
--- a/lib/identity_config.rb
+++ b/lib/identity_config.rb
@@ -421,6 +421,8 @@ def self.store
config.add(:recommend_webauthn_platform_for_sms_ab_test_authentication_percent, type: :integer)
config.add(:socure_docv_document_request_endpoint, type: :string)
config.add(:socure_docv_enabled, type: :boolean)
+ config.add(:socure_docv_verification_data_test_mode, type: :boolean)
+ config.add(:socure_docv_verification_data_test_mode_tokens, type: :json)
config.add(:socure_docv_webhook_secret_key_queue, type: :json)
config.add(:socure_docv_webhook_secret_key, type: :string)
config.add(:socure_idplus_api_key, type: :string)
diff --git a/lib/reporting/protocols_report.rb b/lib/reporting/protocols_report.rb
index fe1d333961a..560d7da79d7 100644
--- a/lib/reporting/protocols_report.rb
+++ b/lib/reporting/protocols_report.rb
@@ -17,6 +17,7 @@ class ProtocolsReport
attr_reader :time_range
SAML_AUTH_EVENT = 'SAML Auth'
+ SAML_AUTH_REQUEST_EVENT = 'SAML Auth Request'
OIDC_AUTH_EVENT = 'OpenID Connect: authorization request'
# @param [Range] time_range
@@ -352,7 +353,7 @@ def protocol_query
def saml_signature_query
params = {
- events: quote([SAML_AUTH_EVENT]),
+ events: quote([SAML_AUTH_REQUEST_EVENT]),
}
format(<<~QUERY, params)
@@ -362,7 +363,6 @@ def saml_signature_query
properties.event_properties.request_signed != 1 AS not_signed,
isempty(properties.event_properties.matching_cert_serial) AND signed AS invalid_signature
| filter name IN %{events}
- AND properties.event_properties.success = 1
| stats
sum(not_signed) AS unsigned_count,
sum(invalid_signature) AS invalid_signature_count
diff --git a/spec/config/initializers/ab_tests_spec.rb b/spec/config/initializers/ab_tests_spec.rb
index 724b71e32df..fc2c57eb656 100644
--- a/spec/config/initializers/ab_tests_spec.rb
+++ b/spec/config/initializers/ab_tests_spec.rb
@@ -3,6 +3,10 @@
RSpec.describe AbTests do
include AbTestsHelper
+ after :suite do
+ reload_ab_tests
+ end
+
describe '#all' do
it 'returns all registered A/B tests' do
expect(AbTests.all.values).to all(be_kind_of(AbTest))
diff --git a/spec/controllers/concerns/ial2_profile_concern_spec.rb b/spec/controllers/concerns/ial2_profile_concern_spec.rb
new file mode 100644
index 00000000000..66fdc0dbbc2
--- /dev/null
+++ b/spec/controllers/concerns/ial2_profile_concern_spec.rb
@@ -0,0 +1,213 @@
+require 'rails_helper'
+
+RSpec.describe Ial2ProfileConcern do
+ let(:test_controller) do
+ Class.new do
+ include Ial2ProfileConcern
+
+ attr_accessor :current_user, :user_session, :analytics
+
+ def initialize(current_user:, user_session:, analytics:)
+ @current_user = current_user
+ @user_session = user_session
+ @analytics = analytics
+ end
+ end
+ end
+
+ let(:user) { create(:user) }
+ let(:user_session) { {} }
+ let(:analytics) { double(Analytics) }
+ let(:encryption_error) { Encryption::EncryptionError.new }
+ let(:cacher) { double(Pii::Cacher) }
+ let(:password) { 'PraiseTheSun!' }
+
+ describe '#cache_profiles' do
+ subject { test_controller.new(current_user: user, user_session:, analytics:) }
+
+ context 'when the user has a pending profile' do
+ let!(:profile) { create(:profile, :in_person_verification_pending, user:) }
+
+ context 'when the profile can be saved to cache' do
+ before do
+ allow(cacher).to receive(:save)
+ allow(Pii::Cacher).to receive(:new).and_return(cacher)
+ subject.cache_profiles(password)
+ end
+
+ it 'stores the decrypted profile in cache' do
+ expect(cacher).to have_received(:save).with(password, profile)
+ end
+ end
+
+ context 'when the profile can not be saved to cache' do
+ before do
+ allow(cacher).to receive(:save).and_raise(encryption_error)
+ allow(Pii::Cacher).to receive(:new).and_return(cacher)
+ allow(analytics).to receive(:profile_encryption_invalid)
+ subject.cache_profiles(password)
+ end
+
+ it 'deactivates the profile with reason encryption_error' do
+ expect(profile.reload).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ )
+ end
+
+ it 'logs the profile_encryption_invalid analytic' do
+ expect(analytics).to have_received(:profile_encryption_invalid).with(
+ error: encryption_error.message,
+ )
+ end
+ end
+ end
+
+ context 'when the user has an active profile' do
+ let!(:profile) { create(:profile, :active, user:) }
+
+ context 'when the profile can be saved to cache' do
+ before do
+ allow(cacher).to receive(:save)
+ allow(Pii::Cacher).to receive(:new).and_return(cacher)
+ subject.cache_profiles(password)
+ end
+
+ it 'stores the decrypted profile in cache' do
+ expect(cacher).to have_received(:save).with(password, profile)
+ end
+ end
+
+ context 'when the profile can not be saved to cache' do
+ before do
+ allow(cacher).to receive(:save).and_raise(encryption_error)
+ allow(Pii::Cacher).to receive(:new).and_return(cacher)
+ allow(analytics).to receive(:profile_encryption_invalid)
+ subject.cache_profiles(password)
+ end
+
+ it 'deactivates the profile with reason encryption_error' do
+ expect(profile.reload).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ )
+ end
+
+ it 'logs the profile_encryption_invalid analytic' do
+ expect(analytics).to have_received(:profile_encryption_invalid).with(
+ error: encryption_error.message,
+ )
+ end
+ end
+ end
+
+ context 'when the user has both an active profile and pending profile' do
+ let(:pending_profile) { create(:profile, :in_person_verification_pending, user:) }
+ let(:active_profile) { create(:profile, :active, user:) }
+
+ context 'when the active profile was activated before the pending profile was created' do
+ before do
+ pending_profile.update!(created_at: Time.zone.now)
+ active_profile.update!(activated_at: 1.day.ago)
+ end
+
+ context 'when the profiles can be saved to cache' do
+ before do
+ allow(cacher).to receive(:save)
+ allow(Pii::Cacher).to receive(:new).and_return(cacher)
+ subject.cache_profiles(password)
+ end
+
+ it 'stores the decrypted pending profile in cache' do
+ expect(cacher).to have_received(:save).with(password, pending_profile)
+ end
+
+ it 'stores the decrypted active profile in cache' do
+ expect(cacher).to have_received(:save).with(password, active_profile)
+ end
+ end
+
+ context 'when the profile can not be saved to cache' do
+ before do
+ allow(cacher).to receive(:save).and_raise(encryption_error)
+ allow(Pii::Cacher).to receive(:new).and_return(cacher)
+ allow(analytics).to receive(:profile_encryption_invalid)
+ subject.cache_profiles(password)
+ end
+
+ it 'deactivates the pending profile with reason encryption_error' do
+ expect(pending_profile.reload).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ )
+ end
+
+ it 'deactivates the active profile with reason encryption_error' do
+ expect(active_profile.reload).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ )
+ end
+
+ it 'logs the profile_encryption_invalid analytic' do
+ expect(analytics).to have_received(:profile_encryption_invalid).with(
+ error: encryption_error.message,
+ ).twice
+ end
+ end
+ end
+
+ context 'when the active profile was activated after the pending profile was created' do
+ before do
+ pending_profile.update!(created_at: 1.day.ago)
+ active_profile.update!(activated_at: Time.zone.now)
+ end
+
+ context 'when the profiles can be saved to cache' do
+ before do
+ allow(cacher).to receive(:save)
+ allow(Pii::Cacher).to receive(:new).and_return(cacher)
+ subject.cache_profiles(password)
+ end
+
+ it 'does not store the decrypted pending profile in cache' do
+ expect(cacher).not_to have_received(:save).with(password, pending_profile)
+ end
+
+ it 'stores the decrypted active profile in cache' do
+ expect(cacher).to have_received(:save).with(password, active_profile)
+ end
+ end
+
+ context 'when the profile can not be saved to cache' do
+ before do
+ allow(cacher).to receive(:save).and_raise(encryption_error)
+ allow(Pii::Cacher).to receive(:new).and_return(cacher)
+ allow(analytics).to receive(:profile_encryption_invalid)
+ subject.cache_profiles(password)
+ end
+
+ it 'does not deactivate the pending profile with reason encryption_error' do
+ expect(pending_profile.reload).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ )
+ end
+
+ it 'deactivates the active profile with reason encryption_error' do
+ expect(active_profile.reload).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ )
+ end
+
+ it 'logs the profile_encryption_invalid analytic' do
+ expect(analytics).to have_received(:profile_encryption_invalid).with(
+ error: encryption_error.message,
+ ).once
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/idv/account_verified_cta_visited_controller_spec.rb b/spec/controllers/idv/account_verified_cta_visited_controller_spec.rb
index 338e3b1b709..8600a739930 100644
--- a/spec/controllers/idv/account_verified_cta_visited_controller_spec.rb
+++ b/spec/controllers/idv/account_verified_cta_visited_controller_spec.rb
@@ -15,12 +15,12 @@
describe 'GET #show' do
subject(:action) { get :show, params: }
- context 'issuer provided and return_to_sp_url present' do
+ context 'issuer provided and post_idv_follow_up_url present' do
let(:service_provider) do
build(
:service_provider,
issuer: 'urn:my:awesome:issuer',
- return_to_sp_url: 'https://some-sp.com',
+ post_idv_follow_up_url: 'https://some-sp.com',
)
end
@@ -28,7 +28,7 @@
{ issuer: 'urn:my:awesome:issuer', campaign_id: '123234234' }
end
- it 'redirects to the service provider home page and logs event' do
+ it 'redirects to the service provider follow_up url and logs event' do
action
aggregate_failures 'verify response' do
@@ -50,6 +50,7 @@
:service_provider,
issuer: 'urn:my:awesome:issuer',
return_to_sp_url: nil,
+ post_idv_follow_up_url: nil,
acs_url: nil,
redirect_uris: nil,
)
diff --git a/spec/controllers/idv/by_mail/sp_follow_up_controller_spec.rb b/spec/controllers/idv/by_mail/sp_follow_up_controller_spec.rb
new file mode 100644
index 00000000000..9005379e672
--- /dev/null
+++ b/spec/controllers/idv/by_mail/sp_follow_up_controller_spec.rb
@@ -0,0 +1,83 @@
+require 'rails_helper'
+
+RSpec.describe Idv::ByMail::SpFollowUpController do
+ let(:post_idv_follow_up_url) { 'https://example.com/follow_up' }
+ let(:initiating_service_provider) { create(:service_provider, post_idv_follow_up_url:) }
+ let(:user) { create(:user, :fully_registered) }
+ let!(:profile) { create(:profile, :active, user:, initiating_service_provider:) }
+
+ before do
+ stub_sign_in(user) if user.present?
+ stub_analytics
+ end
+
+ describe '#new' do
+ context 'the user has not finished verification' do
+ let(:profile) do
+ create(:profile, :verify_by_mail_pending, user:, initiating_service_provider:)
+ end
+
+ it 'redirects to the account page' do
+ get :new
+
+ expect(response).to redirect_to(account_url)
+ end
+ end
+
+ context 'the user has an SP in the session' do
+ before do
+ allow(controller).to receive(:current_sp).and_return(initiating_service_provider)
+ end
+
+ it 'redirects to the account page' do
+ get :new
+
+ expect(response).to redirect_to(account_url)
+ end
+ end
+
+ context 'the user does not have an initiating service provider' do
+ let(:profile) { create(:profile, :active, user:, initiating_service_provider: nil) }
+
+ it 'redirects to the account page' do
+ get :new
+
+ expect(response).to redirect_to(account_url)
+ end
+ end
+
+ it 'logs analytics and renders the template' do
+ get :new
+
+ expect(response).to render_template(:new)
+ expect(@analytics).to have_logged_event(
+ :idv_by_mail_sp_follow_up_visited,
+ initiating_service_provider: initiating_service_provider.issuer,
+ )
+ end
+ end
+
+ describe '#show' do
+ it 'logs analytics and redirects to the service provider' do
+ get :show
+
+ expect(response).to redirect_to(post_idv_follow_up_url)
+ expect(@analytics).to have_logged_event(
+ :idv_by_mail_sp_follow_up_submitted,
+ initiating_service_provider: initiating_service_provider.issuer,
+ )
+ end
+ end
+
+ describe '#cancel' do
+ it 'logs analytics and redirects to the account URL' do
+ get :cancel
+
+ expect(response).to redirect_to(account_url)
+ expect(@analytics).to have_logged_event(
+ :idv_by_mail_sp_follow_up_cancelled,
+ initiating_service_provider: initiating_service_provider.issuer,
+ )
+ end
+ end
+end
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 636e81c430b..c9cfce2b570 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
@@ -18,6 +18,8 @@
end
let(:document_capture_session_uuid) { document_capture_session&.uuid }
+ let(:socure_docv_verification_data_test_mode) { false }
+
before do
allow(IdentityConfig.store).to receive(:socure_docv_enabled).
and_return(socure_docv_enabled)
@@ -33,6 +35,14 @@
session[:doc_capture_user_id] = user&.id
session[:document_capture_session_uuid] = document_capture_session_uuid
+ allow(IdentityConfig.store).
+ to receive(:socure_docv_verification_data_test_mode).
+ and_return(socure_docv_verification_data_test_mode)
+
+ unless IdentityConfig.store.socure_docv_verification_data_test_mode
+ expect(IdentityConfig.store).not_to receive(:socure_docv_verification_data_test_mode_tokens)
+ end
+
stub_analytics
end
@@ -133,6 +143,12 @@
)
end
+ it 'logs correct info' do
+ expect(@analytics).to have_logged_event(
+ :idv_socure_document_request_submitted,
+ )
+ 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)
@@ -359,5 +375,48 @@
end
end
end
+
+ context 'when socure_docv_verification_data_test_mode is enabled' do
+ let(:test_token) { '12345' }
+ let(:socure_docv_verification_data_test_mode) { true }
+
+ before do
+ ActiveJob::Base.queue_adapter = :test
+ allow(IdentityConfig.store).
+ to receive(:socure_docv_verification_data_test_mode_tokens).
+ and_return([test_token])
+
+ stub_request(
+ :post,
+ "#{IdentityConfig.store.socure_idplus_base_url}/api/3.0/EmailAuthScore",
+ ).
+ with(body: { modules: ['documentverification'], docvTransactionToken: test_token }.
+ to_json).
+ to_return(
+ headers: {
+ 'Content-Type' => 'application/json',
+ },
+ body: SocureDocvFixtures.pass_json,
+ )
+ end
+
+ context 'when a token is provided from the allow list' do
+ it 'performs SocureDocvResultsJob' do
+ expect { get(:update, params: { docv_token: test_token }) }.
+ not_to have_enqueued_job(SocureDocvResultsJob) # is synchronous
+
+ expect(document_capture_session.reload.load_result).not_to be_nil
+ end
+ end
+
+ context 'when a token is provided not on the allow list' do
+ it 'performs SocureDocvResultsJob' do
+ expect { get(:update, params: { docv_token: 'rando-token' }) }.
+ not_to have_enqueued_job(SocureDocvResultsJob)
+
+ expect(document_capture_session.reload.load_result).to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb
index 8238bc1b0f6..6f37ec787c1 100644
--- a/spec/controllers/idv/personal_key_controller_spec.rb
+++ b/spec/controllers/idv/personal_key_controller_spec.rb
@@ -483,14 +483,43 @@ def assert_personal_key_generated_for_profiles(*profile_pii_pairs)
context 'user selected gpo verification' do
let(:address_verification_mechanism) { 'gpo' }
- it 'redirects to correct url' do
- patch :update
- expect(response).to redirect_to idv_letter_enqueued_url
+ context 'when the user requested a letter this session' do
+ it 'redirects to correct url' do
+ patch :update
+ expect(response).to redirect_to idv_letter_enqueued_url
+ end
+
+ it 'does not log any events' do
+ expect(@analytics).not_to have_logged_event
+ patch :update
+ end
end
- it 'does not log any events' do
- expect(@analytics).not_to have_logged_event
- patch :update
+ context 'when the user entered a GPO code' do
+ before do
+ pending_profile = user.pending_profile
+ pending_profile.remove_gpo_deactivation_reason
+ pending_profile.activate
+ end
+
+ it 'redirects to correct url' do
+ patch :update
+ expect(response).to redirect_to idv_sp_follow_up_url
+ end
+
+ it 'logs analytics' do
+ patch :update
+
+ expect(@analytics).to have_logged_event(
+ 'IdV: personal key submitted',
+ hash_including(
+ address_verification_method: 'gpo',
+ fraud_review_pending: false,
+ fraud_rejection: false,
+ in_person_verification_pending: false,
+ ),
+ )
+ end
end
end
diff --git a/spec/controllers/idv/socure/document_capture_controller_spec.rb b/spec/controllers/idv/socure/document_capture_controller_spec.rb
index 09b00461415..7d494be9516 100644
--- a/spec/controllers/idv/socure/document_capture_controller_spec.rb
+++ b/spec/controllers/idv/socure/document_capture_controller_spec.rb
@@ -27,6 +27,8 @@
)
end
+ let(:socure_docv_verification_data_test_mode) { false }
+
before do
allow(IdentityConfig.store).to receive(:socure_docv_enabled).
and_return(socure_docv_enabled)
@@ -44,6 +46,13 @@
allow(subject).to receive(:user_session).and_return(user_session)
subject.idv_session.document_capture_session_uuid = document_capture_session.uuid
+ allow(IdentityConfig.store).
+ to receive(:socure_docv_verification_data_test_mode).
+ and_return(socure_docv_verification_data_test_mode)
+
+ unless IdentityConfig.store.socure_docv_verification_data_test_mode
+ expect(IdentityConfig.store).not_to receive(:socure_docv_verification_data_test_mode_tokens)
+ end
stub_analytics
end
@@ -146,6 +155,12 @@
)
end
+ it 'logs correct info' do
+ expect(@analytics).to have_logged_event(
+ :idv_socure_document_request_submitted,
+ )
+ 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)
@@ -341,6 +356,49 @@
expect(response.body).to eq('Technical difficulties!!!')
end
end
+
+ context 'when socure_docv_verification_data_test_mode is enabled' do
+ let(:test_token) { '12345' }
+ let(:socure_docv_verification_data_test_mode) { true }
+
+ before do
+ ActiveJob::Base.queue_adapter = :test
+ allow(IdentityConfig.store).
+ to receive(:socure_docv_verification_data_test_mode_tokens).
+ and_return([test_token])
+
+ stub_request(
+ :post,
+ "#{IdentityConfig.store.socure_idplus_base_url}/api/3.0/EmailAuthScore",
+ ).
+ with(body: { modules: ['documentverification'], docvTransactionToken: test_token }.
+ to_json).
+ to_return(
+ headers: {
+ 'Content-Type' => 'application/json',
+ },
+ body: SocureDocvFixtures.pass_json,
+ )
+ end
+
+ context 'when a token is provided from the allow list' do
+ it 'performs SocureDocvResultsJob' do
+ expect { get(:update, params: { docv_token: test_token }) }.
+ not_to have_enqueued_job(SocureDocvResultsJob) # is synchronous
+
+ expect(document_capture_session.reload.load_result).not_to be_nil
+ end
+ end
+
+ context 'when a token is provided not on the allow list' do
+ it 'performs SocureDocvResultsJob' do
+ expect { get(:update, params: { docv_token: 'rando-token' }) }.
+ not_to have_enqueued_job(SocureDocvResultsJob)
+
+ expect(document_capture_session.reload.load_result).to be_nil
+ end
+ end
+ end
end
context 'when socure is disabled' do
diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb
index c2f6cacea8d..b5ad4de18da 100644
--- a/spec/controllers/saml_idp_controller_spec.rb
+++ b/spec/controllers/saml_idp_controller_spec.rb
@@ -1334,6 +1334,39 @@ def name_id_version(format_urn)
end
end
+ context 'when service provider has no certs' do
+ let(:service_provider) do
+ create(
+ :service_provider,
+ certs: [],
+ active: true,
+ )
+ end
+
+ let(:settings) do
+ saml_settings.tap do |settings|
+ settings.issuer = service_provider.issuer
+ end
+ end
+
+ it 'returns an error page' do
+ user = create(:user, :fully_registered)
+ stub_analytics
+
+ generate_saml_response(user, settings)
+
+ expect(response.body).to include(t('errors.messages.no_cert_registered'))
+ expect(@analytics).to have_logged_event(
+ 'SAML Auth',
+ hash_including(
+ success: false,
+ errors: { service_provider: [t('errors.messages.no_cert_registered')] },
+ error_details: { service_provider: { no_cert_registered: true } },
+ ),
+ )
+ end
+ end
+
context 'service provider has multiple certs' do
let(:service_provider) do
create(
diff --git a/spec/controllers/users/webauthn_platform_recommended_controller_spec.rb b/spec/controllers/users/webauthn_platform_recommended_controller_spec.rb
index 853b2b9fe18..13d2c027caf 100644
--- a/spec/controllers/users/webauthn_platform_recommended_controller_spec.rb
+++ b/spec/controllers/users/webauthn_platform_recommended_controller_spec.rb
@@ -2,8 +2,14 @@
RSpec.describe Users::WebauthnPlatformRecommendedController do
let(:user) { create(:user) }
+ let(:current_sp) { create(:service_provider) }
before do
+ controller.session[:sp] = {
+ issuer: current_sp.issuer,
+ acr_values: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF,
+ request_url: 'http://example.com',
+ }
stub_sign_in(user) if user
end
@@ -72,12 +78,25 @@
context 'user is creating account' do
before do
allow(controller).to receive(:in_account_creation_flow?).and_return(true)
- allow(controller).to receive(:next_setup_path).and_return(sign_up_completed_path)
+ controller.user_session[:mfa_selections] = []
end
- it 'redirects user to set up next authenticator' do
+ it 'redirects user to consent screen' do
expect(response).to redirect_to(sign_up_completed_path)
end
+
+ context 'mfa selections already completed' do
+ # Regression: If duplicate submission occurs (e.g. pressing back button), selections is
+ # already cleared from session, but the user is still in the account creation flow.
+
+ before do
+ controller.user_session[:mfa_selections] = nil
+ end
+
+ it 'redirects user to consent screen' do
+ expect(response).to redirect_to(sign_up_completed_path)
+ end
+ end
end
context 'user opted to add' do
diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb
index c5694c47a20..519ba225ac5 100644
--- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb
+++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb
@@ -385,7 +385,7 @@ def verify_no_upload_photos_section_and_link(page)
end
describe 'when selfie is required by sp' do
before do
- click_on t('forms.buttons.continue_remote_selfie')
+ click_on t('forms.buttons.continue_remote_mobile')
end
it 'shows selfie version of top content and ipp option section' do
verify_handoff_page_selfie_version_content(page)
diff --git a/spec/features/idv/doc_auth/socure_document_capture_spec.rb b/spec/features/idv/doc_auth/socure_document_capture_spec.rb
index b645ea89562..7b49d706609 100644
--- a/spec/features/idv/doc_auth/socure_document_capture_spec.rb
+++ b/spec/features/idv/doc_auth/socure_document_capture_spec.rb
@@ -11,6 +11,7 @@
let(:socure_docv_webhook_secret_key) { 'socure_docv_webhook_secret_key' }
let(:fake_socure_docv_document_request_endpoint) { 'https://fake-socure.test/document-request' }
let(:fake_socure_document_capture_app_url) { 'https://verify.fake-socure.test/something' }
+ let(:socure_docv_verification_data_test_mode) { false }
before(:each) do
allow(IdentityConfig.store).to receive(:socure_docv_enabled).and_return(true)
@@ -24,23 +25,19 @@
allow(IdentityConfig.store).to receive(:ruby_workers_idv_enabled).and_return(false)
allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics)
@docv_transaction_token = stub_docv_document_request
+ allow(IdentityConfig.store).to receive(:socure_docv_verification_data_test_mode).
+ and_return(socure_docv_verification_data_test_mode)
end
- before(:all) do
- @user = user_with_2fa
- end
-
- after(:all) { @user.destroy }
-
- context 'happy path' do
+ context 'happy path', allow_browser_log: true do
before do
- stub_docv_verification_data_pass
+ @pass_stub = stub_docv_verification_data_pass(docv_transaction_token: @docv_transaction_token)
end
context 'standard desktop flow' do
before do
visit_idp_from_oidc_sp_with_ial2
- sign_in_and_2fa_user(@user)
+ @user = sign_in_and_2fa_user
complete_doc_auth_steps_before_document_capture_step
click_idv_continue
end
@@ -66,6 +63,9 @@
'Rate Limit Reached',
limiter_type: :idv_doc_auth,
)
+ expect(fake_analytics).to have_logged_event(
+ :idv_socure_document_request_submitted,
+ )
end
context 'successfully processes image on last attempt' do
@@ -91,7 +91,7 @@
end
end
- context 'network connection errors' do
+ context 'network connection errors', allow_browser_log: true do
context 'getting the capture path' do
before do
allow_any_instance_of(Faraday::Connection).to receive(:post).
@@ -106,6 +106,9 @@
expect(page).to have_content(t('doc_auth.headers.general.network_error'))
expect(page).to have_content(t('doc_auth.errors.general.new_network_error'))
+ expect(fake_analytics).to have_logged_event(
+ :idv_socure_document_request_submitted,
+ )
end
end
@@ -116,12 +119,29 @@
end
end
+ context 'invalid request', allow_browser_log: true do
+ context 'getting the capture path w wrong api key' do
+ before do
+ DocAuth::Mock::DocAuthMockClient.reset!
+ stub_docv_document_request(status: 401)
+ end
+
+ it 'correctly logs event', js: true do
+ visit idv_socure_document_capture_path
+ expect(fake_analytics).to have_logged_event(
+ :idv_socure_document_request_submitted,
+ )
+ end
+ end
+ end
+
it 'does not track state if state tracking is disabled' do
allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(false)
socure_docv_upload_documents(
docv_transaction_token: @docv_transaction_token,
)
+ visit idv_socure_document_capture_update_path
expect(DocAuthLog.find_by(user_id: @user.id).state).to be_nil
end
@@ -131,15 +151,68 @@
docv_transaction_token: @docv_transaction_token,
)
+ visit idv_socure_document_capture_update_path
expect(DocAuthLog.find_by(user_id: @user.id).state).not_to be_nil
end
+
+ context 'when socure_docv_verification_data_test_mode is enabled' do
+ let(:test_token) { 'valid-test-token' }
+ let(:socure_docv_verification_data_test_mode) { true }
+ before do
+ allow(IdentityConfig.store).to receive(:socure_docv_verification_data_test_mode_tokens).
+ and_return([test_token])
+ DocAuth::Mock::DocAuthMockClient.reset!
+ end
+
+ context 'when a valid test token is used' do
+ it 'fetches verificationdata using override docvToken in request',
+ allow_browser_log: true do
+ remove_request_stub(@pass_stub)
+ stub_docv_verification_data_pass(docv_transaction_token: test_token)
+
+ visit idv_socure_document_capture_update_path(docv_token: test_token)
+ expect(page).to have_current_path(idv_ssn_url)
+
+ expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY')
+
+ fill_out_ssn_form_ok
+ click_idv_continue
+ complete_verify_step
+ expect(page).to have_current_path(idv_phone_url)
+ end
+ end
+
+ context 'when an invalid test token is used' do
+ let(:invalid_token) { 'invalid-token' }
+ it 'waits to fetch verificationdata using docv capture session token' do
+ visit idv_socure_document_capture_update_path(docv_token: invalid_token)
+
+ expect(page).to have_current_path(
+ idv_socure_document_capture_update_path(docv_token: invalid_token),
+ )
+ socure_docv_upload_documents(
+ docv_transaction_token: @docv_transaction_token,
+ )
+ visit idv_socure_document_capture_update_path(docv_token: invalid_token)
+
+ expect(page).to have_current_path(idv_ssn_url)
+
+ expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY')
+
+ fill_out_ssn_form_ok
+ click_idv_continue
+ complete_verify_step
+ expect(page).to have_current_path(idv_phone_url)
+ end
+ end
+ end
end
context 'standard mobile flow' do
it 'proceeds to the next page with valid info' do
perform_in_browser(:mobile) do
visit_idp_from_oidc_sp_with_ial2
- sign_in_and_2fa_user(@user)
+ @user = sign_in_and_2fa_user
complete_doc_auth_steps_before_document_capture_step
expect(page).to have_current_path(idv_socure_document_capture_url)
@@ -152,6 +225,9 @@
expect(page).to have_current_path(idv_ssn_url)
expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY')
+ expect(fake_analytics).to have_logged_event(
+ :idv_socure_document_request_submitted,
+ )
fill_out_ssn_form_ok
click_idv_continue
@@ -164,10 +240,13 @@
shared_examples 'a properly categorized Socure error' do |socure_error_code, expected_header_key|
before do
- stub_docv_verification_data_fail_with([socure_error_code])
+ stub_docv_verification_data_fail_with(
+ docv_transaction_token: @docv_transaction_token,
+ errors: [socure_error_code],
+ )
visit_idp_from_oidc_sp_with_ial2
- sign_in_and_2fa_user(@user)
+ @user = sign_in_and_2fa_user
complete_doc_auth_steps_before_document_capture_step
@@ -180,6 +259,9 @@
it 'shows the correct error page' do
expect(page).to have_content(t(expected_header_key))
+ expect(fake_analytics).to have_logged_event(
+ :idv_socure_document_request_submitted,
+ )
end
end
diff --git a/spec/features/idv/get_proofing_results_job_scenarios_spec.rb b/spec/features/idv/get_proofing_results_job_scenarios_spec.rb
index 08be64fae88..f28bc7fe37c 100644
--- a/spec/features/idv/get_proofing_results_job_scenarios_spec.rb
+++ b/spec/features/idv/get_proofing_results_job_scenarios_spec.rb
@@ -51,9 +51,9 @@
# Then the user is taken to the /verify/welcome page
expect(current_path).to eq(idv_welcome_path)
- # And the user has an InPersonEnrollment with status "pending"
+ # And the user has an InPersonEnrollment with status "cancelled"
expect(@user.in_person_enrollments.first).to have_attributes(
- status: 'pending',
+ status: 'cancelled',
)
# And the user has a Profile that is deactivated with reason "encryption_error"
expect(@user.in_person_enrollments.first.profile).to have_attributes(
@@ -78,7 +78,7 @@
expect(@user.in_person_enrollments.first.profile).to have_attributes(
active: false,
deactivation_reason: 'encryption_error',
- in_person_verification_pending_at: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
)
# When the user logs in
@@ -94,7 +94,7 @@
expect(@user.in_person_enrollments.first.profile).to have_attributes(
active: false,
deactivation_reason: 'encryption_error',
- in_person_verification_pending_at: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
)
end
@@ -130,9 +130,9 @@
# Then the user is taken to the /verify/welcome page
expect(current_path).to eq(idv_welcome_path)
- # And the user has an InPersonEnrollment with status "pending"
+ # And the user has an InPersonEnrollment with status "cancelled"
expect(@user.in_person_enrollments.first).to have_attributes(
- status: 'pending',
+ status: 'cancelled',
)
# And the user has a Profile that is deactivated with reason "encryption_error"
expect(@user.in_person_enrollments.first.profile).to have_attributes(
@@ -157,7 +157,7 @@
expect(@user.in_person_enrollments.first.profile).to have_attributes(
active: false,
deactivation_reason: 'encryption_error',
- in_person_verification_pending_at: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
)
# When the user logs in
@@ -173,7 +173,7 @@
expect(@user.in_person_enrollments.first.profile).to have_attributes(
active: false,
deactivation_reason: 'encryption_error',
- in_person_verification_pending_at: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
)
end
end
@@ -537,9 +537,9 @@
# Then the user is taken to the /verify/welcome page
expect(current_path).to eq(idv_welcome_path)
- # And the user has an InPersonEnrollment with status "pending"
+ # And the user has an InPersonEnrollment with status "cancelled"
expect(@user.in_person_enrollments.first).to have_attributes(
- status: 'pending',
+ status: 'cancelled',
)
# And the user has a Profile that is deactivated with reason "encryption_error" and
# pending in person verification and fraud review
@@ -568,7 +568,7 @@
expect(@user.in_person_enrollments.first.profile).to have_attributes(
active: false,
deactivation_reason: 'encryption_error',
- in_person_verification_pending_at: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
fraud_pending_reason: 'threatmetrix_review',
fraud_review_pending_at: be_kind_of(Time),
)
@@ -587,7 +587,7 @@
expect(@user.in_person_enrollments.first.profile).to have_attributes(
active: false,
deactivation_reason: 'encryption_error',
- in_person_verification_pending_at: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
fraud_pending_reason: 'threatmetrix_review',
fraud_review_pending_at: be_kind_of(Time),
)
diff --git a/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb
index 0245ba60c5a..4560fa0d52e 100644
--- a/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb
+++ b/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb
@@ -9,6 +9,8 @@
let(:sp) { :oidc }
let(:fake_socure_document_capture_app_url) { 'https://verify.fake-socure.test/something' }
let(:fake_socure_docv_document_request_endpoint) { 'https://fake-socure.test/document-request' }
+ let(:socure_docv_verification_data_test_mode) { false }
+ let(:fake_analytics) { FakeAnalytics.new }
before do
allow(FeatureManagement).to receive(:doc_capture_polling_enabled?).and_return(true)
@@ -23,10 +25,16 @@
@sms_link = config[:link]
impl.call(**config)
end.at_least(1).times
+ allow(IdentityConfig.store).to receive(:socure_docv_verification_data_test_mode).
+ and_return(socure_docv_verification_data_test_mode)
@docv_transaction_token = stub_docv_document_request
+ stub_analytics
end
- context 'happy path' do
+ context 'happy path', allow_browser_log: true do
+ before do
+ @pass_stub = stub_docv_verification_data_pass(docv_transaction_token: @docv_transaction_token)
+ end
it 'proofs and hands off to mobile', js: true do
user = nil
@@ -75,8 +83,6 @@
visit idv_hybrid_mobile_socure_document_capture_url
expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url)
-
- stub_docv_verification_data_pass
click_idv_continue
expect(page).to have_current_path(fake_socure_document_capture_app_url)
socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token)
@@ -96,7 +102,7 @@
perform_in_browser(:desktop) do
expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10)
expect(page).to have_current_path(idv_ssn_path)
-
+ expect(@analytics).to have_logged_event(:idv_socure_document_request_submitted)
fill_out_ssn_form_ok
click_idv_continue
@@ -190,7 +196,6 @@
click_idv_continue
expect(page).to have_current_path(fake_socure_document_capture_app_url)
- stub_docv_verification_data_pass
max_attempts.times do
socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token)
end
@@ -225,7 +230,6 @@
visit @sms_link
expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url)
- stub_docv_verification_data_pass
click_idv_continue
expect(page).to have_current_path(fake_socure_document_capture_app_url)
socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token)
@@ -253,6 +257,87 @@
)
end
end
+
+ context 'when socure_docv_verification_data_test_mode is enabled' do
+ let(:test_token) { 'valid-test-token' }
+ let(:socure_docv_verification_data_test_mode) { true }
+ before do
+ allow(IdentityConfig.store).to receive(:socure_docv_verification_data_test_mode_tokens).
+ and_return([test_token])
+ DocAuth::Mock::DocAuthMockClient.reset!
+ end
+
+ context 'when a valid test token is used' do
+ it 'fetches verificationdata using override docvToken in request',
+ js: true, allow_browser_log: true do
+ user = nil
+
+ perform_in_browser(:desktop) do
+ visit_idp_from_sp_with_ial2(sp)
+ user = sign_up_and_2fa_ial1_user
+
+ complete_doc_auth_steps_before_hybrid_handoff_step
+ click_send_link
+ end
+
+ expect(@sms_link).to be_present
+
+ perform_in_browser(:mobile) do
+ remove_request_stub(@pass_stub)
+ stub_docv_verification_data_pass(docv_transaction_token: test_token)
+
+ visit @sms_link
+
+ expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url)
+
+ visit idv_hybrid_mobile_socure_document_capture_update_path(docv_token: test_token)
+
+ expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
+ end
+
+ perform_in_browser(:desktop) do
+ expect(page).to have_current_path(idv_ssn_path, wait: 10)
+ end
+ end
+ end
+
+ context 'when an invalid test token is used' do
+ let(:invalid_token) { 'invalid-token' }
+
+ it 'waits to fetch verificationdata using docv capture session token', js: true do
+ user = nil
+
+ perform_in_browser(:desktop) do
+ visit_idp_from_sp_with_ial2(sp)
+ user = sign_up_and_2fa_ial1_user
+
+ complete_doc_auth_steps_before_hybrid_handoff_step
+ click_send_link
+ end
+
+ expect(@sms_link).to be_present
+
+ perform_in_browser(:mobile) do
+ visit @sms_link
+
+ click_idv_continue
+
+ visit idv_hybrid_mobile_socure_document_capture_update_path(docv_token: invalid_token)
+ expect(page).to have_current_path(
+ idv_hybrid_mobile_socure_document_capture_update_path(docv_token: invalid_token),
+ )
+ socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token)
+ visit idv_hybrid_mobile_socure_document_capture_update_path(docv_token: invalid_token)
+
+ expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_path)
+ end
+
+ perform_in_browser(:desktop) do
+ expect(page).to have_current_path(idv_ssn_path, wait: 10)
+ end
+ end
+ end
+ end
end
shared_examples 'a properly categorized Socure error' do |socure_error_code, expected_header_key|
@@ -280,7 +365,10 @@
perform_in_browser(:mobile) do
visit @sms_link
- stub_docv_verification_data_fail_with([socure_error_code])
+ stub_docv_verification_data_fail_with(
+ docv_transaction_token: @docv_transaction_token,
+ errors: [socure_error_code],
+ )
click_idv_continue
@@ -354,6 +442,7 @@
expect(page).to have_text(t('doc_auth.headers.general.network_error'))
expect(page).to have_text(t('doc_auth.errors.general.new_network_error'))
+ expect(@analytics).to have_logged_event(:idv_socure_document_request_submitted)
end
perform_in_browser(:desktop) do
@@ -361,4 +450,25 @@
end
end
end
+
+ context 'invalid request', allow_browser_log: true do
+ context 'getting the capture path w wrong api key' do
+ before do
+ user = user_with_2fa
+ visit_idp_from_oidc_sp_with_ial2
+ sign_in_and_2fa_user(user)
+ complete_doc_auth_steps_before_document_capture_step
+ click_idv_continue
+ DocAuth::Mock::DocAuthMockClient.reset!
+ stub_docv_document_request(status: 401)
+ end
+
+ it 'correctly logs event', js: true do
+ visit idv_socure_document_capture_path
+ expect(@analytics).to have_logged_event(
+ :idv_socure_document_request_submitted,
+ )
+ end
+ end
+ end
end
diff --git a/spec/features/idv/sp_follow_up_spec.rb b/spec/features/idv/sp_follow_up_spec.rb
new file mode 100644
index 00000000000..a8f5da4e6c7
--- /dev/null
+++ b/spec/features/idv/sp_follow_up_spec.rb
@@ -0,0 +1,106 @@
+require 'rails_helper'
+require 'action_account'
+
+RSpec.feature 'returning to an SP after out-of-band proofing' do
+ scenario 'receiving an email after entering a verify-by-mail code' do
+ post_idv_follow_up_url = 'https://example.com/idv_follow_up'
+ initiating_service_provider = create(:service_provider, post_idv_follow_up_url:)
+ profile = create(:profile, :verify_by_mail_pending, :with_pii, initiating_service_provider:)
+ user = profile.user
+ otp = 'ABC123'
+ create(
+ :gpo_confirmation_code,
+ profile: profile,
+ otp_fingerprint: Pii::Fingerprinter.fingerprint(otp),
+ created_at: 2.days.ago,
+ updated_at: 2.days.ago,
+ )
+
+ sign_in_live_with_2fa(user)
+
+ expect(current_path).to eq(idv_verify_by_mail_enter_code_path)
+
+ fill_in t('idv.gpo.form.otp_label'), with: otp
+ click_button t('idv.gpo.form.submit')
+ open_last_email
+ click_email_link_matching(/return_to_sp\/account_verified_cta/)
+
+ expect(current_url).to eq(post_idv_follow_up_url)
+ end
+
+ scenario 'receiving an email after passing fraud review' do
+ post_idv_follow_up_url = 'https://example.com/idv_follow_up'
+ initiating_service_provider = create(:service_provider, post_idv_follow_up_url:)
+ profile = create(:profile, :fraud_review_pending, :with_pii, initiating_service_provider:)
+ user = profile.user
+
+ expect(FraudReviewChecker.new(user).fraud_review_pending?).to eq(true)
+
+ review_pass = ActionAccount::ReviewPass.new
+ review_pass_config = ScriptBase::Config.new(reason: 'feature-test')
+ review_pass.run(args: [user.uuid], config: review_pass_config)
+
+ open_last_email
+ click_email_link_matching(/return_to_sp\/account_verified_cta/)
+
+ expect(current_url).to eq(post_idv_follow_up_url)
+ end
+
+ context 'after entering a verify-by-mail code' do
+ scenario 'clicking on the CTA' do
+ post_idv_follow_up_url = 'https://example.com/idv_follow_up'
+ initiating_service_provider = create(:service_provider, post_idv_follow_up_url:)
+ profile = create(:profile, :verify_by_mail_pending, :with_pii, initiating_service_provider:)
+ user = profile.user
+ otp = 'ABC123'
+ create(
+ :gpo_confirmation_code,
+ profile: profile,
+ otp_fingerprint: Pii::Fingerprinter.fingerprint(otp),
+ created_at: 2.days.ago,
+ updated_at: 2.days.ago,
+ )
+
+ sign_in_live_with_2fa(user)
+
+ expect(current_path).to eq(idv_verify_by_mail_enter_code_path)
+
+ fill_in t('idv.gpo.form.otp_label'), with: otp
+ click_button t('idv.gpo.form.submit')
+ acknowledge_and_confirm_personal_key
+
+ expect(current_path).to eq(idv_sp_follow_up_path)
+ click_on t('idv.by_mail.sp_follow_up.connect_account')
+
+ expect(current_url).to eq(post_idv_follow_up_url)
+ end
+
+ scenario 'canceling on the CTA' do
+ post_idv_follow_up_url = 'https://example.com/idv_follow_up'
+ initiating_service_provider = create(:service_provider, post_idv_follow_up_url:)
+ profile = create(:profile, :verify_by_mail_pending, :with_pii, initiating_service_provider:)
+ user = profile.user
+ otp = 'ABC123'
+ create(
+ :gpo_confirmation_code,
+ profile: profile,
+ otp_fingerprint: Pii::Fingerprinter.fingerprint(otp),
+ created_at: 2.days.ago,
+ updated_at: 2.days.ago,
+ )
+
+ sign_in_live_with_2fa(user)
+
+ expect(current_path).to eq(idv_verify_by_mail_enter_code_path)
+
+ fill_in t('idv.gpo.form.otp_label'), with: otp
+ click_button t('idv.gpo.form.submit')
+ acknowledge_and_confirm_personal_key
+
+ expect(current_path).to eq(idv_sp_follow_up_path)
+ click_on t('idv.by_mail.sp_follow_up.go_to_account')
+
+ expect(current_url).to eq(account_url)
+ end
+ end
+end
diff --git a/spec/fixtures/proofing/aamva/requests/verification_request.xml b/spec/fixtures/proofing/aamva/requests/verification_request.xml
index c42882043d4..ef7dfd1243f 100644
--- a/spec/fixtures/proofing/aamva/requests/verification_request.xml
+++ b/spec/fixtures/proofing/aamva/requests/verification_request.xml
@@ -31,7 +31,8 @@
VA
20176
- 1
+ 1
+
diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb
index 57f9631c173..336c6eee4d4 100644
--- a/spec/models/profile_spec.rb
+++ b/spec/models/profile_spec.rb
@@ -609,6 +609,68 @@
end
end
+ describe '#deactivate_due_to_encryption_error' do
+ context 'when the profile has a "pending" in_person_enrollment' do
+ subject { create(:profile, :in_person_verification_pending, user: user) }
+ let!(:enrollment) do
+ create(:in_person_enrollment, user: user, profile: subject, status: :pending)
+ end
+
+ before do
+ subject.deactivate_due_to_encryption_error
+ end
+
+ it 'deactivates with reason encryption_error' do
+ expect(subject).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ in_person_verification_pending_at: be_kind_of(Time),
+ )
+ end
+
+ it 'cancels the associated pending in_person_enrollment' do
+ expect(subject.in_person_enrollment.status).to eq('cancelled')
+ end
+ end
+
+ context 'when the profile has a "passed" in_person_enrollment' do
+ subject { create(:profile, :active, user: user) }
+ let!(:enrollment) do
+ create(:in_person_enrollment, user: user, profile: subject, status: :passed)
+ end
+
+ before do
+ subject.deactivate_due_to_encryption_error
+ end
+
+ it 'deactivates with reason encryption_error' do
+ expect(subject).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ )
+ end
+
+ it 'does not cancel the associated pending in_person_enrollment' do
+ expect(subject.in_person_enrollment.status).to eq('passed')
+ end
+ end
+
+ context 'when the profile has no in_person_enrollment' do
+ subject { create(:profile, :active, user: user) }
+
+ before do
+ subject.deactivate_due_to_encryption_error
+ end
+
+ it 'deactivates with reason encryption_error' do
+ expect(subject).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ )
+ end
+ end
+ end
+
describe '#remove_gpo_deactivation_reason' do
it 'removes the gpo_verification_pending_at deactivation reason' do
profile = create(:profile, :verify_by_mail_pending)
diff --git a/spec/presenters/idv/account_verified_email_presenter_spec.rb b/spec/presenters/idv/account_verified_email_presenter_spec.rb
index f31f64dc681..27d34501786 100644
--- a/spec/presenters/idv/account_verified_email_presenter_spec.rb
+++ b/spec/presenters/idv/account_verified_email_presenter_spec.rb
@@ -57,11 +57,12 @@
end
context 'where there is a service provider' do
- context 'when the service provider has no return URL' do
+ context 'when the service provider has no post-IdV follow-up URL' do
let(:service_provider) do
create(
:service_provider,
issuer: 'urn:my:awesome:sp',
+ post_idv_follow_up_url: nil,
return_to_sp_url: nil,
friendly_name: 'My Awesome SP',
)
@@ -101,12 +102,12 @@
end
end
- context 'when the service provider does have a return URL' do
+ context 'when the service provider does have a post-IdV follow-up URL' do
let(:service_provider) do
create(
:service_provider,
issuer: 'urn:my:awesome:sp',
- return_to_sp_url: 'https://www.mysp.com',
+ post_idv_follow_up_url: 'https://www.mysp.com',
friendly_name: 'My Awesome SP',
)
end
diff --git a/spec/presenters/idv/by_mail/sp_follow_up_presenter_spec.rb b/spec/presenters/idv/by_mail/sp_follow_up_presenter_spec.rb
new file mode 100644
index 00000000000..cf58b449906
--- /dev/null
+++ b/spec/presenters/idv/by_mail/sp_follow_up_presenter_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+RSpec.describe Idv::ByMail::SpFollowUpPresenter do
+ let(:sp_name) { 'Test SP' }
+ let(:service_provider) { create(:service_provider, friendly_name: sp_name) }
+ let(:user) do
+ create(
+ :profile,
+ :active,
+ initiating_service_provider: service_provider,
+ ).user
+ end
+
+ subject(:presenter) { described_class.new(current_user: user) }
+
+ describe '#heading' do
+ it 'interpolates the SP name' do
+ expect(presenter.heading).to eq(
+ t('idv.by_mail.sp_follow_up.heading', service_provider: sp_name),
+ )
+ end
+ end
+
+ describe '#body' do
+ it 'interpolates the SP name' do
+ expect(presenter.body).to eq(
+ t('idv.by_mail.sp_follow_up.body', service_provider: sp_name, app_name: APP_NAME),
+ )
+ end
+ end
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 95b8bcb97e6..4dc85032e9f 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -44,6 +44,7 @@
config.include AnalyticsHelper
config.include AwsCloudwatchHelper
config.include AwsKmsClientHelper
+ config.include DiffHelper
config.include KeyRotationHelper
config.include OtpHelper
config.include XmlHelper
diff --git a/spec/services/proofing/aamva/request/verification_request_spec.rb b/spec/services/proofing/aamva/request/verification_request_spec.rb
index 187b766fd01..79b52ef2716 100644
--- a/spec/services/proofing/aamva/request/verification_request_spec.rb
+++ b/spec/services/proofing/aamva/request/verification_request_spec.rb
@@ -33,7 +33,7 @@
describe '#body' do
it 'should be a request body' do
- expect(subject.body + "\n").to eq(AamvaFixtures.verification_request)
+ expect(subject.body).to match_xml(AamvaFixtures.verification_request)
end
it 'should escape XML in applicant data' do
diff --git a/spec/services/saml_request_validator_spec.rb b/spec/services/saml_request_validator_spec.rb
index bf530ff771d..c6c3e512a7a 100644
--- a/spec/services/saml_request_validator_spec.rb
+++ b/spec/services/saml_request_validator_spec.rb
@@ -49,6 +49,29 @@
end
end
+ context 'when the sp has no certs registered' do
+ before { sp.update!(certs: nil) }
+ let(:errors) do
+ {
+ service_provider: [t('errors.messages.no_cert_registered')],
+ }
+ end
+ let(:error_details) do
+ {
+ service_provider: {
+ no_cert_registered: true,
+ },
+ }
+ end
+
+ it 'returns an error' do
+ expect(response.to_h).to include(
+ errors:,
+ error_details:,
+ )
+ end
+ end
+
context 'ialmax authncontext and ialmax provider' do
let(:authn_context) { [Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF] }
diff --git a/spec/services/sp_return_url_resolver_spec.rb b/spec/services/sp_return_url_resolver_spec.rb
index 30fc0aa8cc0..3181cb88723 100644
--- a/spec/services/sp_return_url_resolver_spec.rb
+++ b/spec/services/sp_return_url_resolver_spec.rb
@@ -140,4 +140,35 @@
end
end
end
+
+ describe '#post_idv_follow_up_url' do
+ let(:return_to_sp_url) { nil }
+ let(:sp_post_idv_follow_up_url) { nil }
+ let(:sp) do
+ build(
+ :service_provider,
+ return_to_sp_url: return_to_sp_url,
+ post_idv_follow_up_url: sp_post_idv_follow_up_url,
+ )
+ end
+ let(:instance) { described_class.new(service_provider: sp) }
+ subject(:post_idv_follow_up_url) { instance.post_idv_follow_up_url }
+
+ context 'with not homepage url or follow_up url configured' do
+ it { expect(post_idv_follow_up_url).to be_nil }
+ end
+
+ context 'with a homepage url configured' do
+ let(:return_to_sp_url) { 'https://sp.gov/return_to_sp' }
+
+ it { expect(post_idv_follow_up_url).to eq(return_to_sp_url) }
+ end
+
+ context 'with a follow_up url configured' do
+ let(:return_to_sp_url) { 'https://sp.gov/return_to_sp' }
+ let(:sp_post_idv_follow_up_url) { 'https://sp.gov/follow_up' }
+
+ it { expect(post_idv_follow_up_url).to eq(sp_post_idv_follow_up_url) }
+ end
+ end
end
diff --git a/spec/support/deprecated_classes.rb b/spec/support/deprecated_classes.rb
index 333b672b558..16e595e48e1 100644
--- a/spec/support/deprecated_classes.rb
+++ b/spec/support/deprecated_classes.rb
@@ -1,7 +1,7 @@
class ActionView::Helpers::TagHelper::TagBuilder
def self.deprecated_classes
@deprecated_classes ||= begin
- YAML.safe_load(File.read(File.expand_path('../../../.erb-lint.yml', __FILE__))).
+ YAML.safe_load(File.read(File.expand_path('../../../.erb_lint.yml', __FILE__))).
dig('linters', 'DeprecatedClasses', 'rule_set').
flat_map { |rule| rule['deprecated'] }.
map { |regex_str| Regexp.new "^#{regex_str}$" }
diff --git a/spec/support/diff_helper.rb b/spec/support/diff_helper.rb
new file mode 100644
index 00000000000..be56abc0a99
--- /dev/null
+++ b/spec/support/diff_helper.rb
@@ -0,0 +1,14 @@
+module DiffHelper
+ def assert_error_messages_equal(err, expected)
+ actual = normalize_error_message(err.message)
+ expected = normalize_error_message(expected)
+ expect(actual).to eql(expected)
+ end
+
+ def normalize_error_message(message)
+ message.
+ gsub(/\x1b\[[0-9;]*m/, ''). # Strip ANSI control characters used for color
+ gsub(/:0x[0-9a-f]{16}/, ':').
+ strip
+ end
+end
diff --git a/spec/support/fake_analytics_spec.rb b/spec/support/fake_analytics_spec.rb
index ef2dda16d5f..e1b9291ea28 100644
--- a/spec/support/fake_analytics_spec.rb
+++ b/spec/support/fake_analytics_spec.rb
@@ -647,17 +647,4 @@
)
end
end
-
- def assert_error_messages_equal(err, expected)
- actual = normalize_error_message(err.message)
- expected = normalize_error_message(expected)
- expect(actual).to eql(expected)
- end
-
- def normalize_error_message(message)
- message.
- gsub(/\x1b\[[0-9;]*m/, ''). # Strip ANSI control characters used for color
- gsub(/:0x[0-9a-f]{16}/, ':').
- strip
- end
end
diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb
index bf374379bbd..cd948410a74 100644
--- a/spec/support/features/doc_auth_helper.rb
+++ b/spec/support/features/doc_auth_helper.rb
@@ -98,7 +98,7 @@ def complete_up_to_how_to_verify_step_for_opt_in_ipp(remote: true,
complete_agreement_step
if remote
if facial_match_required
- click_on t('forms.buttons.continue_remote_selfie')
+ click_on t('forms.buttons.continue_remote_mobile')
else
click_on t('forms.buttons.continue_remote')
end
diff --git a/spec/support/features/document_capture_step_helper.rb b/spec/support/features/document_capture_step_helper.rb
index 9143136c3fe..72c40807522 100644
--- a/spec/support/features/document_capture_step_helper.rb
+++ b/spec/support/features/document_capture_step_helper.rb
@@ -103,16 +103,22 @@ def socure_docv_send_webhook(
end
end
- def stub_docv_verification_data_pass
- stub_docv_verification_data(body: SocureDocvFixtures.pass_json)
+ def stub_docv_verification_data_pass(docv_transaction_token:)
+ stub_docv_verification_data(body: SocureDocvFixtures.pass_json, docv_transaction_token:)
end
- def stub_docv_verification_data_fail_with(errors)
- stub_docv_verification_data(body: SocureDocvFixtures.fail_json(errors))
+ def stub_docv_verification_data_fail_with(docv_transaction_token:, errors:)
+ stub_docv_verification_data(body: SocureDocvFixtures.fail_json(errors), docv_transaction_token:)
end
- def stub_docv_verification_data(body:)
+ def stub_docv_verification_data(docv_transaction_token:, body:)
+ request_body = {
+ modules: ['documentverification'],
+ docvTransactionToken: docv_transaction_token,
+ }
+
stub_request(:post, "#{IdentityConfig.store.socure_idplus_base_url}/api/3.0/EmailAuthScore").
+ with(body: request_body.to_json).
to_return(
headers: {
'Content-Type' => 'application/json',
diff --git a/spec/support/xml_helper.rb b/spec/support/xml_helper.rb
index eaa46261f3e..25107cbd326 100644
--- a/spec/support/xml_helper.rb
+++ b/spec/support/xml_helper.rb
@@ -21,4 +21,13 @@ def delete_xml_at_xpath(xml, xpath)
element.parent.delete(element)
document.to_s
end
+
+ def pretty_xml_from_string(document)
+ output = String.new(encoding: 'UTF-8')
+ doc = REXML::Document.new(document)
+ formatter = REXML::Formatters::Pretty.new
+ formatter.compact = true
+ formatter.write(doc, output)
+ output.to_s
+ end
end
diff --git a/spec/support/xml_matcher.rb b/spec/support/xml_matcher.rb
new file mode 100644
index 00000000000..55b80342457
--- /dev/null
+++ b/spec/support/xml_matcher.rb
@@ -0,0 +1,18 @@
+RSpec::Matchers.define :match_xml do |comparison|
+ diffable
+
+ # REXML::Documents do not implement comparison; they are never ==.
+ # This matcher considers the documents the same if their string outputs
+ # are equal after both going through the REXML::Formatters::Pretty.
+ match do |document|
+ # We have to override these for the diff to use these, rather than input strings
+ @actual = XmlHelper.pretty_xml_from_string(document)
+ @expected = XmlHelper.pretty_xml_from_string(comparison)
+
+ expect(@actual).to eq(@expected)
+ end
+
+ failure_message do
+ 'Expected XML documents to be the same, but they differed:'
+ end
+end
diff --git a/spec/support/xml_matcher_spec.rb b/spec/support/xml_matcher_spec.rb
new file mode 100644
index 00000000000..fda2b643764
--- /dev/null
+++ b/spec/support/xml_matcher_spec.rb
@@ -0,0 +1,146 @@
+require 'rails_helper'
+
+RSpec.describe 'match_xml custom matcher' do
+ let(:weirdly_formatted_carrot_xml) do
+ <<~XML
+
+ Carrot Orange (usually)
+
+
+
+
+ XML
+ end
+
+ let(:normally_formatted_carrot_xml) do
+ <<~XML
+
+ Carrot
+ Orange (usually)
+
+ XML
+ end
+
+ let(:carrot_xml_in_a_different_order) do
+ <<~XML
+
+ Orange (usually)
+ Carrot
+
+ XML
+ end
+
+ let(:all_caps_carrot_xml) do
+ <<~XML
+
+ Carrot
+ Orange (usually)
+
+ XML
+ end
+
+ let(:tubers_xml) do
+ <<~XML
+
+
+
+
+ XML
+ end
+
+ describe 'match_xml' do
+ context 'when the documents are identical' do
+ it 'matches' do
+ expect(weirdly_formatted_carrot_xml).to match_xml(weirdly_formatted_carrot_xml)
+ end
+ end
+
+ context 'when the documents are formatted differently but have the same content' do
+ it 'matches' do
+ expect(weirdly_formatted_carrot_xml).to match_xml(normally_formatted_carrot_xml)
+ end
+ end
+
+ context 'when there is a case difference between tags' do
+ let(:code_under_test) do
+ -> { expect(normally_formatted_carrot_xml).to match_xml(all_caps_carrot_xml) }
+ end
+
+ it 'does not match' do
+ expect(normally_formatted_carrot_xml).to_not match_xml(all_caps_carrot_xml)
+ end
+
+ it 'generates a useful diff' do
+ expect(&code_under_test).to raise_error(RSpec::Expectations::ExpectationNotMetError) do |e|
+ assert_error_messages_equal(e, <<~ERROR)
+ Expected XML documents to be the same, but they differed:
+ Diff:
+
+ @@ -1,5 +1,5 @@
+ -
+ +
+ Carrot
+ Orange (usually)
+ -
+ +
+ ERROR
+ end
+ end
+ end
+
+ context 'when the data is the same but ordered differently' do
+ let(:code_under_test) do
+ -> { expect(normally_formatted_carrot_xml).to match_xml(carrot_xml_in_a_different_order) }
+ end
+
+ # This illustrates current behavior, but is not necessarily a mandate.
+ it 'does not match' do
+ expect(normally_formatted_carrot_xml).not_to match_xml(carrot_xml_in_a_different_order)
+ end
+
+ it 'generates a useful diff' do
+ expect(&code_under_test).to raise_error(RSpec::Expectations::ExpectationNotMetError) do |e|
+ assert_error_messages_equal(e, <<~ERROR)
+ Expected XML documents to be the same, but they differed:
+ Diff:
+
+ @@ -1,5 +1,5 @@
+
+ - Orange (usually)
+ Carrot
+ + Orange (usually)
+
+ ERROR
+ end
+ end
+ end
+
+ context 'when the documents are wholly different' do
+ let(:code_under_test) do
+ -> { expect(normally_formatted_carrot_xml).to match_xml(tubers_xml) }
+ end
+
+ it 'does not match' do
+ expect(normally_formatted_carrot_xml).not_to match_xml(tubers_xml)
+ end
+
+ it 'generates a useful diff' do
+ expect(&code_under_test).to raise_error(RSpec::Expectations::ExpectationNotMetError) do |e|
+ assert_error_messages_equal(e, <<~ERROR)
+ Expected XML documents to be the same, but they differed:
+ Diff:
+ @@ -1,5 +1,5 @@
+ -
+ -
+ -
+ -
+ +
+ + Carrot
+ + Orange (usually)
+ +
+ ERROR
+ end
+ end
+ end
+ end
+end
diff --git a/spec/views/idv/how_to_verify/show.html.erb_spec.rb b/spec/views/idv/how_to_verify/show.html.erb_spec.rb
index 4e7b1e6cfad..1a3d6ec7d42 100644
--- a/spec/views/idv/how_to_verify/show.html.erb_spec.rb
+++ b/spec/views/idv/how_to_verify/show.html.erb_spec.rb
@@ -2,8 +2,14 @@
RSpec.describe 'idv/how_to_verify/show.html.erb' do
selection = Idv::HowToVerifyForm::IPP
+ let(:mobile_required) { false }
let(:selfie_check_required) { false }
- let(:presenter) { Idv::HowToVerifyPresenter.new(selfie_check_required: selfie_check_required) }
+ let(:presenter) do
+ Idv::HowToVerifyPresenter.new(
+ mobile_required: mobile_required,
+ selfie_check_required: selfie_check_required,
+ )
+ end
let(:idv_how_to_verify_form) { Idv::HowToVerifyForm.new(selection: selection) }
before do
@@ -11,9 +17,10 @@
assign(:presenter, presenter)
assign :idv_how_to_verify_form, idv_how_to_verify_form
end
- context 'when selfie is not required' do
+ context 'when mobile is not required' do
before do
- @selfie_required = false
+ @mobile_required = mobile_required
+ @selfie_required = selfie_check_required
render
end
@@ -61,11 +68,13 @@
end
end
- context 'when selfie is required' do
- let(:selfie_check_required) { true }
+ context 'when mobile is required' do
+ let(:selfie_check_required) { false }
+ let(:mobile_required) { true }
before do
- @selfie_required = true
+ @selfie_required = selfie_check_required
+ @mobile_required = mobile_required
render
end
@@ -81,12 +90,12 @@
end
it 'renders two options for verifying your identity' do
- expect(rendered).to have_content(t('doc_auth.headings.verify_online_selfie'))
+ expect(rendered).to have_content(t('doc_auth.headings.verify_online_mobile'))
expect(rendered).to have_content(t('doc_auth.headings.verify_at_post_office'))
end
it 'renders a button for remote and ipp' do
- expect(rendered).to have_button(t('forms.buttons.continue_remote_selfie'))
+ expect(rendered).to have_button(t('forms.buttons.continue_remote_mobile'))
expect(rendered).to have_button(t('forms.buttons.continue_ipp'))
end
@@ -102,17 +111,27 @@
expect(rendered).to have_link(t('links.cancel'))
end
- it 'renders selfie specific content' do
- expect(rendered).to have_content(t('doc_auth.info.how_to_verify_selfie'))
- expect(rendered).to have_content(t('doc_auth.tips.mobile_phone_required'))
- expect(rendered).to have_content(t('doc_auth.info.verify_online_instruction_selfie'))
- expect(rendered).to have_content(t('doc_auth.info.verify_online_description_selfie'))
- expect(rendered).to have_content(
- t('doc_auth.info.verify_at_post_office_instruction_selfie'),
- )
+ it 'renders mobile specific content' do
expect(rendered).to have_content(
- t('doc_auth.info.verify_at_post_office_description_selfie'),
+ t('doc_auth.info.verify_online_instruction_mobile_no_selfie'),
)
end
+
+ context 'when selfie is required' do
+ let(:selfie_check_required) { true }
+
+ it 'renders selfie specific content' do
+ expect(rendered).to have_content(t('doc_auth.info.how_to_verify_mobile'))
+ expect(rendered).to have_content(t('doc_auth.tips.mobile_phone_required'))
+ expect(rendered).to have_content(t('doc_auth.info.verify_online_instruction_selfie'))
+ expect(rendered).to have_content(t('doc_auth.info.verify_online_description_mobile'))
+ expect(rendered).to have_content(
+ t('doc_auth.info.verify_at_post_office_instruction_selfie'),
+ )
+ expect(rendered).to have_content(
+ t('doc_auth.info.verify_at_post_office_description_mobile'),
+ )
+ end
+ end
end
end
diff --git a/spec/views/users/webauthn_platform_recommended/new.html.erb_spec.rb b/spec/views/users/webauthn_platform_recommended/new.html.erb_spec.rb
index 951ef168c63..fb217054a22 100644
--- a/spec/views/users/webauthn_platform_recommended/new.html.erb_spec.rb
+++ b/spec/views/users/webauthn_platform_recommended/new.html.erb_spec.rb
@@ -3,6 +3,11 @@
RSpec.describe 'users/webauthn_platform_recommended/new.html.erb' do
subject(:rendered) { render }
+ it 'renders separate forms with submission for options to add' do
+ expect(rendered).to have_css('form:has(input[name=add_method]):has([type=submit])')
+ expect(rendered).to have_css('form:not(:has(input[name=add_method])):has([type=submit])')
+ end
+
it 'renders a help link for phishing-resistant including flow path' do
@sign_in_flow = :example