diff --git a/.rubocop.yml b/.rubocop.yml
index 3b952f5e1c4..c882e675be8 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -28,7 +28,7 @@ AllCops:
- 'vendor/**/*'
- 'public/**/*'
TargetRubyVersion: 3.2.0
- TargetRailsVersion: 7.1
+ TargetRailsVersion: 7.2
UseCache: true
DisabledByDefault: true
SuggestExtensions: false
diff --git a/Gemfile b/Gemfile
index e96adfe135d..70131c68b55 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,7 +3,7 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}.git" }
ruby "~> #{File.read(File.join(__dir__, '.ruby-version')).strip}"
-gem 'rails', '~> 7.1.4'
+gem 'rails', '~> 7.2.1'
gem 'ahoy_matey', '~> 3.0'
# pod identity requires 3.188.0
@@ -117,7 +117,7 @@ group :development, :test do
gem 'pry-rails'
gem 'psych'
gem 'rspec', '~> 3.13.0'
- gem 'rspec-rails', '~> 6.0'
+ gem 'rspec-rails', '~> 7.0'
gem 'rubocop', '~> 1.62.0', require: false
gem 'rubocop-performance', '~> 1.20.2', require: false
gem 'rubocop-rails', '>= 2.26.2', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 9dbc4929498..149c90bb090 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -79,80 +79,76 @@ GIT
GEM
remote: https://rubygems.org/
specs:
- actioncable (7.1.4.1)
- actionpack (= 7.1.4.1)
- activesupport (= 7.1.4.1)
+ actioncable (7.2.1.1)
+ actionpack (= 7.2.1.1)
+ activesupport (= 7.2.1.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
- actionmailbox (7.1.4.1)
- actionpack (= 7.1.4.1)
- activejob (= 7.1.4.1)
- activerecord (= 7.1.4.1)
- activestorage (= 7.1.4.1)
- activesupport (= 7.1.4.1)
- mail (>= 2.7.1)
- net-imap
- net-pop
- net-smtp
- actionmailer (7.1.4.1)
- actionpack (= 7.1.4.1)
- actionview (= 7.1.4.1)
- activejob (= 7.1.4.1)
- activesupport (= 7.1.4.1)
- mail (~> 2.5, >= 2.5.4)
- net-imap
- net-pop
- net-smtp
+ actionmailbox (7.2.1.1)
+ actionpack (= 7.2.1.1)
+ activejob (= 7.2.1.1)
+ activerecord (= 7.2.1.1)
+ activestorage (= 7.2.1.1)
+ activesupport (= 7.2.1.1)
+ mail (>= 2.8.0)
+ actionmailer (7.2.1.1)
+ actionpack (= 7.2.1.1)
+ actionview (= 7.2.1.1)
+ activejob (= 7.2.1.1)
+ activesupport (= 7.2.1.1)
+ mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
- actionpack (7.1.4.1)
- actionview (= 7.1.4.1)
- activesupport (= 7.1.4.1)
+ actionpack (7.2.1.1)
+ actionview (= 7.2.1.1)
+ activesupport (= 7.2.1.1)
nokogiri (>= 1.8.5)
racc
- rack (>= 2.2.4)
+ rack (>= 2.2.4, < 3.2)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
- actiontext (7.1.4.1)
- actionpack (= 7.1.4.1)
- activerecord (= 7.1.4.1)
- activestorage (= 7.1.4.1)
- activesupport (= 7.1.4.1)
+ useragent (~> 0.16)
+ actiontext (7.2.1.1)
+ actionpack (= 7.2.1.1)
+ activerecord (= 7.2.1.1)
+ activestorage (= 7.2.1.1)
+ activesupport (= 7.2.1.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (7.1.4.1)
- activesupport (= 7.1.4.1)
+ actionview (7.2.1.1)
+ activesupport (= 7.2.1.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
- activejob (7.1.4.1)
- activesupport (= 7.1.4.1)
+ activejob (7.2.1.1)
+ activesupport (= 7.2.1.1)
globalid (>= 0.3.6)
- activemodel (7.1.4.1)
- activesupport (= 7.1.4.1)
- activerecord (7.1.4.1)
- activemodel (= 7.1.4.1)
- activesupport (= 7.1.4.1)
+ activemodel (7.2.1.1)
+ activesupport (= 7.2.1.1)
+ activerecord (7.2.1.1)
+ activemodel (= 7.2.1.1)
+ activesupport (= 7.2.1.1)
timeout (>= 0.4.0)
- activestorage (7.1.4.1)
- actionpack (= 7.1.4.1)
- activejob (= 7.1.4.1)
- activerecord (= 7.1.4.1)
- activesupport (= 7.1.4.1)
+ activestorage (7.2.1.1)
+ actionpack (= 7.2.1.1)
+ activejob (= 7.2.1.1)
+ activerecord (= 7.2.1.1)
+ activesupport (= 7.2.1.1)
marcel (~> 1.0)
- activesupport (7.1.4.1)
+ activesupport (7.2.1.1)
base64
bigdecimal
- concurrent-ruby (~> 1.0, >= 1.0.2)
+ concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
+ logger (>= 1.4.2)
minitest (>= 5.1)
- mutex_m
- tzinfo (~> 2.0)
+ securerandom (>= 0.3)
+ tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ahoy_matey (3.3.0)
@@ -395,7 +391,7 @@ GEM
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
- logger (1.6.0)
+ logger (1.6.1)
lograge (0.11.2)
actionpack (>= 4)
activesupport (>= 4)
@@ -433,7 +429,6 @@ GEM
minitest (5.24.1)
msgpack (1.7.2)
multiset (0.5.3)
- mutex_m (0.2.0)
net-http (0.4.1)
uri
net-http-persistent (4.0.2)
@@ -523,20 +518,20 @@ GEM
rackup (2.1.0)
rack (>= 3)
webrick (~> 1.8)
- rails (7.1.4.1)
- actioncable (= 7.1.4.1)
- actionmailbox (= 7.1.4.1)
- actionmailer (= 7.1.4.1)
- actionpack (= 7.1.4.1)
- actiontext (= 7.1.4.1)
- actionview (= 7.1.4.1)
- activejob (= 7.1.4.1)
- activemodel (= 7.1.4.1)
- activerecord (= 7.1.4.1)
- activestorage (= 7.1.4.1)
- activesupport (= 7.1.4.1)
+ rails (7.2.1.1)
+ actioncable (= 7.2.1.1)
+ actionmailbox (= 7.2.1.1)
+ actionmailer (= 7.2.1.1)
+ actionpack (= 7.2.1.1)
+ actiontext (= 7.2.1.1)
+ actionview (= 7.2.1.1)
+ activejob (= 7.2.1.1)
+ activemodel (= 7.2.1.1)
+ activerecord (= 7.2.1.1)
+ activestorage (= 7.2.1.1)
+ activesupport (= 7.2.1.1)
bundler (>= 1.15.0)
- railties (= 7.1.4.1)
+ railties (= 7.2.1.1)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -551,10 +546,10 @@ GEM
rails-i18n (7.0.6)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
- railties (7.1.4.1)
- actionpack (= 7.1.4.1)
- activesupport (= 7.1.4.1)
- irb
+ railties (7.2.1.1)
+ actionpack (= 7.2.1.1)
+ activesupport (= 7.2.1.1)
+ irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
@@ -592,22 +587,22 @@ GEM
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
- rspec-core (3.13.0)
+ rspec-core (3.13.1)
rspec-support (~> 3.13.0)
- rspec-expectations (3.13.0)
+ rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
- rspec-mocks (3.13.0)
+ rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
- rspec-rails (6.0.3)
- actionpack (>= 6.1)
- activesupport (>= 6.1)
- railties (>= 6.1)
- rspec-core (~> 3.12)
- rspec-expectations (~> 3.12)
- rspec-mocks (~> 3.12)
- rspec-support (~> 3.12)
+ rspec-rails (7.0.1)
+ actionpack (>= 7.0)
+ activesupport (>= 7.0)
+ railties (>= 7.0)
+ rspec-core (~> 3.13)
+ rspec-expectations (~> 3.13)
+ rspec-mocks (~> 3.13)
+ rspec-support (~> 3.13)
rspec-retry (0.6.2)
rspec-core (> 3.3)
rspec-support (3.13.1)
@@ -656,6 +651,7 @@ GEM
jwt (~> 2.0)
scrypt (3.0.7)
ffi-compiler (>= 1.0, < 2.0)
+ securerandom (0.3.1)
selenium-webdriver (4.22.0)
base64 (~> 0.2)
logger (~> 1.4)
@@ -706,6 +702,7 @@ GEM
unicode-display_width (2.5.0)
uniform_notifier (1.16.0)
uri (0.13.0)
+ useragent (0.16.10)
view_component (3.9.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
@@ -837,7 +834,7 @@ DEPENDENCIES
rack-test (>= 1.1.0)
rack-timeout
rack_session_access (>= 0.2.0)
- rails (~> 7.1.4)
+ rails (~> 7.2.1)
rails-controller-testing (>= 1.0.4)
redacted_struct
redis (>= 3.2.0)
@@ -847,7 +844,7 @@ DEPENDENCIES
rotp (~> 6.3, >= 6.3.0)
rqrcode
rspec (~> 3.13.0)
- rspec-rails (~> 6.0)
+ rspec-rails (~> 7.0)
rspec-retry
rspec_junit_formatter
rubocop (~> 1.62.0)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 0933f040a98..bf70f35a09b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -230,6 +230,7 @@ def after_sign_in_path_for(_user)
return authentication_methods_setup_url if user_needs_sp_auth_method_setup?
return fix_broken_personal_key_url if current_user.broken_personal_key?
return user_session.delete(:stored_location) if user_session.key?(:stored_location)
+ return setup_piv_cac_url if user_session[:add_piv_cac_after_2fa]
return login_add_piv_cac_prompt_url if session[:needs_to_setup_piv_cac_after_sign_in].present?
return reactivate_account_url if user_needs_to_reactivate_account?
return login_piv_cac_recommended_path if user_recommended_for_piv_cac?
diff --git a/app/controllers/concerns/two_factor_authenticatable_methods.rb b/app/controllers/concerns/two_factor_authenticatable_methods.rb
index a69ecdbfe9f..9a3502ae4ef 100644
--- a/app/controllers/concerns/two_factor_authenticatable_methods.rb
+++ b/app/controllers/concerns/two_factor_authenticatable_methods.rb
@@ -12,16 +12,19 @@ def auth_methods_session
end
def handle_verification_for_authentication_context(result:, auth_method:, extra_analytics: nil)
+ increment_mfa_selection_attempt_count(auth_method)
analytics.multi_factor_auth(
**result.to_h,
multi_factor_auth_method: auth_method,
enabled_mfa_methods_count: mfa_context.enabled_mfa_methods_count,
new_device: new_device?,
**extra_analytics.to_h,
+ attempts: mfa_attempts_count,
)
if result.success?
handle_valid_verification_for_authentication_context(auth_method:)
+ user_session.delete(:mfa_attempts)
else
handle_invalid_verification_for_authentication_context
end
@@ -113,6 +116,20 @@ def handle_remember_device_preference(remember_device_preference)
save_remember_device_preference(remember_device_preference)
end
+ def increment_mfa_selection_attempt_count(auth_method)
+ user_session[:mfa_attempts] ||= {}
+ user_session[:mfa_attempts][:attempts] ||= 0
+ if user_session[:mfa_attempts][:auth_method] != auth_method
+ user_session[:mfa_attempts][:attempts] = 0
+ end
+ user_session[:mfa_attempts][:attempts] += 1
+ user_session[:mfa_attempts][:auth_method] = auth_method
+ end
+
+ def mfa_attempts_count
+ user_session.dig(:mfa_attempts, :attempts)
+ end
+
# Method will be renamed in the next refactor.
# You can pass in any "type" with a corresponding I18n key in
# two_factor_authentication.invalid_#{type}
@@ -137,8 +154,6 @@ def invalid_otp_error(type)
t('two_factor_authentication.invalid_otp')
when 'personal_key'
t('two_factor_authentication.invalid_personal_key')
- when 'piv_cac'
- t('two_factor_authentication.invalid_piv_cac')
else
raise "Unsupported otp method: #{type}"
end
diff --git a/app/controllers/idv/in_person/address_controller.rb b/app/controllers/idv/in_person/address_controller.rb
index 36512fdc59c..a1ca3afd542 100644
--- a/app/controllers/idv/in_person/address_controller.rb
+++ b/app/controllers/idv/in_person/address_controller.rb
@@ -109,11 +109,7 @@ def redirect_to_next_page
def confirm_in_person_state_id_step_complete
return if pii_from_user&.has_key?(:identity_doc_address1)
- if IdentityConfig.store.in_person_state_id_controller_enabled
- redirect_to idv_in_person_proofing_state_id_url
- else
- redirect_to idv_in_person_step_url(step: :state_id)
- end
+ redirect_to idv_in_person_proofing_state_id_url
end
def confirm_in_person_address_step_needed
diff --git a/app/controllers/idv/in_person/public/usps_locations_controller.rb b/app/controllers/idv/in_person/public/usps_locations_controller.rb
index eafddf1924a..b8f20a7f96d 100644
--- a/app/controllers/idv/in_person/public/usps_locations_controller.rb
+++ b/app/controllers/idv/in_person/public/usps_locations_controller.rb
@@ -4,10 +4,6 @@ module Idv
module InPerson
module Public
class UspsLocationsController < ApplicationController
- include RenderConditionConcern
-
- check_or_render_not_found -> { enabled? }
-
skip_forgery_protection
def index
@@ -38,10 +34,6 @@ def localized_locations(locations)
end
end
- def enabled?
- IdentityConfig.store.in_person_public_address_search_enabled
- end
-
def search_params
params.require(:address).permit(
:street_address,
diff --git a/app/controllers/idv/in_person_controller.rb b/app/controllers/idv/in_person_controller.rb
index 49071561e0c..0da218e48f7 100644
--- a/app/controllers/idv/in_person_controller.rb
+++ b/app/controllers/idv/in_person_controller.rb
@@ -20,7 +20,7 @@ class InPersonController < ApplicationController
FLOW_STATE_MACHINE_SETTINGS = {
step_url: :idv_in_person_step_url,
- final_url: :idv_in_person_address_url,
+ final_url: :idv_in_person_proofing_state_id_url,
flow: Idv::Flows::InPersonFlow,
analytics_id: 'In Person Proofing',
}.freeze
diff --git a/app/controllers/two_factor_authentication/options_controller.rb b/app/controllers/two_factor_authentication/options_controller.rb
index fec6acb3a8d..9d74854fad7 100644
--- a/app/controllers/two_factor_authentication/options_controller.rb
+++ b/app/controllers/two_factor_authentication/options_controller.rb
@@ -54,6 +54,7 @@ def two_factor_options_presenter
service_provider: current_sp,
phishing_resistant_required: service_provider_mfa_policy.phishing_resistant_required?,
piv_cac_required: service_provider_mfa_policy.piv_cac_required?,
+ add_piv_cac_after_2fa: user_session[:add_piv_cac_after_2fa].present?,
)
end
diff --git a/app/controllers/two_factor_authentication/otp_verification_controller.rb b/app/controllers/two_factor_authentication/otp_verification_controller.rb
index 728d8620ce6..dee0a141549 100644
--- a/app/controllers/two_factor_authentication/otp_verification_controller.rb
+++ b/app/controllers/two_factor_authentication/otp_verification_controller.rb
@@ -20,6 +20,9 @@ def show
end
def create
+ if UserSessionContext.confirmation_context?(context)
+ increment_mfa_selection_attempt_count(otp_auth_method)
+ end
result = otp_verification_form.submit
post_analytics(result)
@@ -41,6 +44,7 @@ def create
end
reset_otp_session_data
+ user_session.delete(:mfa_attempts)
else
handle_invalid_otp(type: 'otp')
end
@@ -48,6 +52,14 @@ def create
private
+ def otp_auth_method
+ if params[:otp_delivery_preference] == 'sms'
+ TwoFactorAuthenticatable::AuthMethod::SMS
+ else
+ TwoFactorAuthenticatable::AuthMethod::VOICE
+ end
+ end
+
def handle_valid_confirmation_otp
assign_phone
track_mfa_added
@@ -155,6 +167,7 @@ def analytics_properties
phone_configuration_id: phone_configuration&.id,
in_account_creation_flow: user_session[:in_account_creation_flow] || false,
enabled_mfa_methods_count: mfa_context.enabled_mfa_methods_count,
+ attempts: mfa_attempts_count,
}
end
diff --git a/app/controllers/two_factor_authentication/piv_cac_mismatch_controller.rb b/app/controllers/two_factor_authentication/piv_cac_mismatch_controller.rb
new file mode 100644
index 00000000000..a6fe0a3bae8
--- /dev/null
+++ b/app/controllers/two_factor_authentication/piv_cac_mismatch_controller.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module TwoFactorAuthentication
+ class PivCacMismatchController < ApplicationController
+ include TwoFactorAuthenticatable
+
+ def show
+ analytics.piv_cac_mismatch_visited(
+ piv_cac_required: piv_cac_required?,
+ has_other_authentication_methods: has_other_authentication_methods?,
+ )
+
+ @piv_cac_required = piv_cac_required?
+ @has_other_authentication_methods = has_other_authentication_methods?
+ end
+
+ def create
+ analytics.piv_cac_mismatch_submitted(add_piv_cac_after_2fa: add_piv_cac_after_2fa?)
+ user_session[:add_piv_cac_after_2fa] = add_piv_cac_after_2fa?
+ redirect_to login_two_factor_options_url
+ end
+
+ private
+
+ def add_piv_cac_after_2fa?
+ params[:add_piv_cac_after_2fa] == 'true'
+ end
+
+ def piv_cac_required?
+ service_provider_mfa_policy.piv_cac_required?
+ end
+
+ def has_other_authentication_methods?
+ return @has_other_authentication_methods if defined?(@has_other_authentication_methods)
+ @has_other_authentication_methods = mfa_context.two_factor_configurations.any? do |config|
+ config.mfa_enabled? && !config.is_a?(PivCacConfiguration)
+ end
+ end
+
+ def mfa_context
+ @mfa_context ||= MfaContext.new(current_user)
+ end
+ end
+end
diff --git a/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb b/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb
index 27ec7494354..a1fb1a0f5ed 100644
--- a/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb
+++ b/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb
@@ -58,16 +58,22 @@ def handle_valid_piv_cac
def handle_invalid_piv_cac
clear_piv_cac_information
- handle_invalid_otp(type: 'piv_cac')
+ update_invalid_user
+
+ if current_user.locked_out?
+ handle_second_factor_locked_user(type: 'piv_cac')
+ elsif redirect_for_piv_cac_mismatch_replacement?
+ redirect_to login_two_factor_piv_cac_mismatch_url
+ else
+ flash[:error] = t('two_factor_authentication.invalid_piv_cac')
+ redirect_to login_two_factor_piv_cac_url
+ end
end
- # This overrides the method in TwoFactorAuthenticatable so that we
- # redirect back to ourselves rather than rendering the :show template.
- # This removes the token from the address bar and preserves the error
- # in the flash.
- def render_show_after_invalid
- flash[:error] = flash.now[:error]
- redirect_to login_two_factor_piv_cac_url
+ def redirect_for_piv_cac_mismatch_replacement?
+ piv_cac_verification_form.error_type == 'user.piv_cac_mismatch' &&
+ UserSessionContext.authentication_context?(context) &&
+ current_user.piv_cac_configurations.count < IdentityConfig.store.max_piv_cac_per_account
end
def piv_cac_view_data
diff --git a/app/controllers/two_factor_authentication/totp_verification_controller.rb b/app/controllers/two_factor_authentication/totp_verification_controller.rb
index 5577e37a5ed..7eff4f6ac53 100644
--- a/app/controllers/two_factor_authentication/totp_verification_controller.rb
+++ b/app/controllers/two_factor_authentication/totp_verification_controller.rb
@@ -25,7 +25,6 @@ def create
result:,
auth_method: TwoFactorAuthenticatable::AuthMethod::TOTP,
)
-
if result.success?
handle_remember_device_preference(params[:remember_device])
redirect_to after_sign_in_path_for(current_user)
diff --git a/app/controllers/users/piv_cac_authentication_setup_controller.rb b/app/controllers/users/piv_cac_authentication_setup_controller.rb
index 9b32434f741..b52d1e3bad9 100644
--- a/app/controllers/users/piv_cac_authentication_setup_controller.rb
+++ b/app/controllers/users/piv_cac_authentication_setup_controller.rb
@@ -18,6 +18,8 @@ class PivCacAuthenticationSetupController < ApplicationController
helper_method :in_multi_mfa_selection_flow?
def new
+ @piv_cac_required = service_provider_mfa_policy.piv_cac_required?
+
if params.key?(:token)
process_piv_cac_setup
else
@@ -35,7 +37,10 @@ def error
end
def submit_new_piv_cac
- if good_nickname
+ if skip?
+ user_session.delete(:add_piv_cac_after_2fa)
+ redirect_to after_sign_in_path_for(current_user)
+ elsif good_nickname?
user_session[:piv_cac_nickname] = params[:name]
create_piv_cac_nonce
redirect_to piv_cac_service_url_with_redirect, allow_other_host: true
@@ -66,11 +71,13 @@ def piv_cac_service_url_with_redirect
end
def process_piv_cac_setup
+ increment_mfa_selection_attempt_count(TwoFactorAuthenticatable::AuthMethod::PIV_CAC)
result = user_piv_cac_form.submit
properties = result.to_h.merge(analytics_properties)
analytics.multi_factor_auth_setup(**properties)
if result.success?
process_valid_submission
+ user_session.delete(:mfa_attempts)
else
process_invalid_submission
end
@@ -98,6 +105,7 @@ def process_valid_submission
)
create_user_event(:piv_cac_enabled)
track_mfa_method_added
+ user_session.delete(:add_piv_cac_after_2fa)
session[:needs_to_setup_piv_cac_after_sign_in] = false
redirect_to next_setup_path || after_sign_in_path_for(current_user)
end
@@ -117,7 +125,11 @@ def process_invalid_submission
end
end
- def good_nickname
+ def skip?
+ params[:skip] == 'true'
+ end
+
+ def good_nickname?
name = params[:name]
name.present? && !PivCacConfiguration.exists?(user_id: current_user.id, name: name)
end
@@ -126,6 +138,7 @@ def analytics_properties
{
in_account_creation_flow: user_session[:in_account_creation_flow] || false,
enabled_mfa_methods_count: mfa_context.enabled_mfa_methods_count,
+ attempts: mfa_attempts_count,
}
end
diff --git a/app/controllers/users/totp_setup_controller.rb b/app/controllers/users/totp_setup_controller.rb
index fb9524a8be1..af09ac734fe 100644
--- a/app/controllers/users/totp_setup_controller.rb
+++ b/app/controllers/users/totp_setup_controller.rb
@@ -26,12 +26,13 @@ def new
def confirm
result = totp_setup_form.submit
-
+ increment_mfa_selection_attempt_count(TwoFactorAuthenticatable::AuthMethod::TOTP)
properties = result.to_h.merge(analytics_properties)
analytics.multi_factor_auth_setup(**properties)
if result.success?
process_valid_code
+ user_session.delete(:mfa_attempts)
else
process_invalid_code
end
@@ -118,6 +119,7 @@ def analytics_properties
{
in_account_creation_flow: in_account_creation_flow?,
pii_like_keypaths: [[:mfa_method_counts, :phone]],
+ attempts: mfa_attempts_count,
}
end
end
diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb
index 6a80cfda56b..60bb0fc4952 100644
--- a/app/controllers/users/webauthn_setup_controller.rb
+++ b/app/controllers/users/webauthn_setup_controller.rb
@@ -43,6 +43,7 @@ def new
@need_to_set_up_additional_mfa = need_to_set_up_additional_mfa?
if result.errors.present?
+ increment_mfa_selection_attempt_count(webauthn_auth_method)
analytics.webauthn_setup_submitted(
platform_authenticator: form.platform_authenticator?,
errors: result.errors,
@@ -54,6 +55,7 @@ def new
end
def confirm
+ increment_mfa_selection_attempt_count(webauthn_auth_method)
form = WebauthnSetupForm.new(
user: current_user,
user_session: user_session,
@@ -71,9 +73,9 @@ def confirm
)
properties = result.to_h.merge(analytics_properties)
analytics.multi_factor_auth_setup(**properties)
-
if result.success?
process_valid_webauthn(form)
+ user_session.delete(:mfa_attempts)
else
flash.now[:error] = result.first_error_message
render :new
@@ -89,6 +91,14 @@ def validate_existing_platform_authenticator
end
end
+ def webauthn_auth_method
+ if @platform_authenticator
+ TwoFactorAuthenticatable::AuthMethod::WEBAUTHN_PLATFORM
+ else
+ TwoFactorAuthenticatable::AuthMethod::WEBAUTHN
+ end
+ end
+
def platform_authenticator?
params[:platform] == 'true'
end
@@ -141,6 +151,7 @@ def process_valid_webauthn(form)
def analytics_properties
{
in_account_creation_flow: user_session[:in_account_creation_flow] || false,
+ attempts: mfa_attempts_count,
}
end
diff --git a/app/jobs/fraud_rejection_daily_job.rb b/app/jobs/fraud_rejection_daily_job.rb
index 86635c3c419..b99aa9e0258 100644
--- a/app/jobs/fraud_rejection_daily_job.rb
+++ b/app/jobs/fraud_rejection_daily_job.rb
@@ -6,7 +6,7 @@ class FraudRejectionDailyJob < ApplicationJob
def perform(_date)
profiles_eligible_for_fraud_rejection.find_each do |profile|
profile.reject_for_fraud(notify_user: false)
- analytics.automatic_fraud_rejection(
+ analytics(user: profile.user).automatic_fraud_rejection(
fraud_rejection_at: profile.fraud_rejection_at,
)
end
@@ -14,11 +14,11 @@ def perform(_date)
private
- def analytics(user: AnonymousUser.new)
+ def analytics(user:)
Analytics.new(user: user, request: nil, session: {}, sp: nil)
end
def profiles_eligible_for_fraud_rejection
- Profile.where(fraud_review_pending_at: ..30.days.ago)
+ Profile.includes(:user).where(fraud_review_pending_at: ..30.days.ago)
end
end
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 7fe5e30640d..c69079ba73a 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -273,7 +273,7 @@ def in_person_completion_survey
end
end
- def in_person_deadline_passed(enrollment:, visited_location_name:)
+ def in_person_deadline_passed(enrollment:, visited_location_name: nil)
with_user_locale(user) do
@header = t('user_mailer.in_person_deadline_passed.header')
@presenter = Idv::InPerson::VerificationResultsEmailPresenter.new(
@@ -338,7 +338,7 @@ def in_person_ready_to_verify_reminder(enrollment:)
end
end
- def in_person_verified(enrollment:, visited_location_name:)
+ def in_person_verified(enrollment:, visited_location_name: nil)
with_user_locale(user) do
@hide_title = true
@presenter = Idv::InPerson::VerificationResultsEmailPresenter.new(
@@ -353,7 +353,7 @@ def in_person_verified(enrollment:, visited_location_name:)
end
end
- def in_person_failed(enrollment:, visited_location_name:)
+ def in_person_failed(enrollment:, visited_location_name: nil)
with_user_locale(user) do
@presenter = Idv::InPerson::VerificationResultsEmailPresenter.new(
enrollment: enrollment,
@@ -367,7 +367,7 @@ def in_person_failed(enrollment:, visited_location_name:)
end
end
- def in_person_failed_fraud(enrollment:, visited_location_name:)
+ def in_person_failed_fraud(enrollment:, visited_location_name: nil)
with_user_locale(user) do
@presenter = Idv::InPerson::VerificationResultsEmailPresenter.new(
enrollment: enrollment,
@@ -381,7 +381,7 @@ def in_person_failed_fraud(enrollment:, visited_location_name:)
end
end
- def in_person_please_call(enrollment:, visited_location_name:)
+ def in_person_please_call(enrollment:, visited_location_name: nil)
with_user_locale(user) do
@presenter = Idv::InPerson::VerificationResultsEmailPresenter.new(
enrollment: enrollment,
diff --git a/app/models/user.rb b/app/models/user.rb
index b5eb603eddd..5d53cc38dac 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -98,7 +98,7 @@ def last_identity
end
def active_identities
- identities.where('session_uuid IS NOT ?', nil).order(last_authenticated_at: :asc) || []
+ identities.where.not(session_uuid: nil).order(last_authenticated_at: :asc) || []
end
def active_profile?
diff --git a/app/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.rb
index cb66efbcdff..981415b8e80 100644
--- a/app/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.rb
+++ b/app/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.rb
@@ -6,6 +6,11 @@ def type
:piv_cac
end
+ def render_in(view_context, &block)
+ @disabled = view_context.user_session.key?(:add_piv_cac_after_2fa)
+ view_context.capture(&block)
+ end
+
def label
t('two_factor_authentication.login_options.piv_cac')
end
@@ -13,5 +18,9 @@ def label
def info
t('two_factor_authentication.login_options.piv_cac_info')
end
+
+ def disabled?
+ @disabled.present?
+ end
end
end
diff --git a/app/presenters/two_factor_login_options_presenter.rb b/app/presenters/two_factor_login_options_presenter.rb
index 3a9488f22dc..bcc13ba0747 100644
--- a/app/presenters/two_factor_login_options_presenter.rb
+++ b/app/presenters/two_factor_login_options_presenter.rb
@@ -4,11 +4,16 @@ class TwoFactorLoginOptionsPresenter < TwoFactorAuthCode::GenericDeliveryPresent
include AccountResetConcern
include ActionView::Helpers::TranslationHelper
- attr_reader :user, :reauthentication_context, :phishing_resistant_required, :piv_cac_required
+ attr_reader :user,
+ :reauthentication_context,
+ :phishing_resistant_required,
+ :piv_cac_required,
+ :add_piv_cac_after_2fa
alias_method :reauthentication_context?, :reauthentication_context
alias_method :phishing_resistant_required?, :phishing_resistant_required
alias_method :piv_cac_required?, :piv_cac_required
+ alias_method :add_piv_cac_after_2fa?, :add_piv_cac_after_2fa
def initialize(
user:,
@@ -16,7 +21,8 @@ def initialize(
reauthentication_context:,
service_provider:,
phishing_resistant_required:,
- piv_cac_required:
+ piv_cac_required:,
+ add_piv_cac_after_2fa:
)
@user = user
@view = view
@@ -24,6 +30,7 @@ def initialize(
@service_provider = service_provider
@phishing_resistant_required = phishing_resistant_required
@piv_cac_required = piv_cac_required
+ @add_piv_cac_after_2fa = add_piv_cac_after_2fa
end
def title
@@ -47,7 +54,7 @@ def info
end
def restricted_options_warning_text
- return if reauthentication_context?
+ return if show_all_options?
if piv_cac_required?
t('two_factor_authentication.aal2_request.piv_cac_only_html', sp_name:)
@@ -60,9 +67,9 @@ def options
return @options if defined?(@options)
mfa = MfaContext.new(user)
- if piv_cac_required? && !reauthentication_context?
+ if piv_cac_required? && !show_all_options?
configurations = mfa.piv_cac_configurations
- elsif phishing_resistant_required? && !reauthentication_context?
+ elsif phishing_resistant_required? && !show_all_options?
configurations = mfa.phishing_resistant_configurations
else
configurations = mfa.two_factor_configurations
@@ -101,6 +108,10 @@ def first_enabled_option_index
private
+ def show_all_options?
+ reauthentication_context? || add_piv_cac_after_2fa?
+ end
+
def account_reset_link
t(
'two_factor_authentication.account_reset.text_html',
diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb
index 2b5f851659e..d7175ac29ce 100644
--- a/app/services/analytics_events.rb
+++ b/app/services/analytics_events.rb
@@ -2785,37 +2785,6 @@ def idv_in_person_proofing_address_visited(
)
end
- # @param ["hybrid","standard"] flow_path Document capture user flow
- # @param [String] step
- # @param [String] analytics_id
- # @param [Boolean] success Whether form validation was successful
- # @param [Hash] errors Errors resulting from form validation
- # @param [Hash] error_details Details for errors that occurred in unsuccessful submission
- # @param [Boolean] same_address_as_id
- # User clicked cancel on update state id page
- def idv_in_person_proofing_cancel_update_state_id(
- success:,
- errors:,
- flow_path: nil,
- step: nil,
- analytics_id: nil,
- error_details: nil,
- same_address_as_id: nil,
- **extra
- )
- track_event(
- 'IdV: in person proofing cancel_update_state_id submitted',
- flow_path:,
- step:,
- analytics_id:,
- success:,
- errors:,
- error_details:,
- same_address_as_id:,
- **extra,
- )
- end
-
# A job to check USPS notifications about in-person enrollment status updates has completed
# @param [Integer] fetched_items items fetched
# @param [Integer] processed_items items fetched and processed
@@ -2885,40 +2854,6 @@ def idv_in_person_proofing_nontransliterable_characters_submitted(
)
end
- # @param ["hybrid","standard"] flow_path Document capture user flow
- # @param [String] step
- # @param [String] analytics_id
- # @param [Boolean] success Whether form validation was successful
- # @param [Hash] errors Errors resulting from form validation
- # @param [Hash] error_details Details for errors that occurred in unsuccessful submission
- # @param [Boolean] same_address_as_id
- # @param [Boolean] opted_in_to_in_person_proofing User opted into in person proofing
- # User submitted state id on redo state id page
- def idv_in_person_proofing_redo_state_id_submitted(
- success:,
- errors:,
- error_details: nil,
- flow_path: nil,
- step: nil,
- analytics_id: nil,
- same_address_as_id: nil,
- opted_in_to_in_person_proofing: nil,
- **extra
- )
- track_event(
- 'IdV: in person proofing redo_state_id submitted',
- flow_path:,
- step:,
- analytics_id:,
- success:,
- errors:,
- error_details:,
- same_address_as_id:,
- opted_in_to_in_person_proofing:,
- **extra,
- )
- end
-
# @param [Boolean] success Whether form validation was successful
# @param [Hash] errors Errors resulting from form validation
# @param [Hash] error_details Details for errors that occurred in unsuccessful submission
@@ -4970,6 +4905,7 @@ def logout_initiated(
# @param [Boolean] new_device Whether the user is authenticating from a new device
# @param [String] multi_factor_auth_method Authentication method used
# @param [String] multi_factor_auth_method_created_at When the authentication method was created
+ # @param [Integer] attempts number of MFA setup attempts
# @param [Integer] auth_app_configuration_id Database ID of authentication app configuration
# @param [Integer] piv_cac_configuration_id Database ID of PIV/CAC configuration
# @param [String] piv_cac_configuration_dn_uuid PIV/CAC X509 distinguished name UUID
@@ -4993,6 +4929,7 @@ def multi_factor_auth(
errors: nil,
error_details: nil,
context: nil,
+ attempts: nil,
multi_factor_auth_method_created_at: nil,
auth_app_configuration_id: nil,
piv_cac_configuration_id: nil,
@@ -5016,6 +4953,7 @@ def multi_factor_auth(
error_details:,
context:,
new_device:,
+ attempts:,
multi_factor_auth_method:,
multi_factor_auth_method_created_at:,
auth_app_configuration_id:,
@@ -5063,10 +5001,12 @@ def multi_factor_auth_added_phone(
# @param [Integer] enabled_mfa_methods_count Number of enabled MFA methods on the account
# @param [Boolean] in_account_creation_flow whether user is going through creation flow
# @param ['piv_cac'] method_name Authentication method added
+ # @param [Integer] attempts number of MFA setup attempts
def multi_factor_auth_added_piv_cac(
enabled_mfa_methods_count:,
in_account_creation_flow:,
method_name: :piv_cac,
+ attempts: nil,
**extra
)
track_event(
@@ -5074,6 +5014,7 @@ def multi_factor_auth_added_piv_cac(
method_name:,
enabled_mfa_methods_count:,
in_account_creation_flow:,
+ attempts:,
**extra,
)
end
@@ -5113,6 +5054,7 @@ def multi_factor_auth_enter_backup_code_visit(context:, **extra)
end
# @param ["authentication", "reauthentication", "confirmation"] context User session context
+ # @param [Integer] attempts number of MFA setup attempts
# @param [String] multi_factor_auth_method
# @param [Boolean] confirmation_for_add_phone
# @param [Integer] phone_configuration_id
@@ -5132,11 +5074,13 @@ def multi_factor_auth_enter_otp_visit(
phone_fingerprint:,
in_account_creation_flow:,
enabled_mfa_methods_count:,
+ attempts: nil,
**extra
)
track_event(
'Multi-Factor Authentication: enter OTP visited',
context:,
+ attempts:,
multi_factor_auth_method:,
confirmation_for_add_phone:,
phone_configuration_id:,
@@ -5312,6 +5256,7 @@ def multi_factor_auth_phone_setup(
# @param [String, nil] key_id PIV/CAC key_id from PKI service
# @param [Hash] mfa_method_counts Hash of MFA method with the number of that method on the account
# @param [Hash] authenticator_data_flags WebAuthn authenticator data flags
+ # @param [Integer] attempts number of MFA setup attempts
# @param [String, nil] aaguid AAGUID value of WebAuthn device
# @param [String[], nil] unknown_transports Array of unrecognized WebAuthn transports, intended to
# be used in case of future specification changes.
@@ -5335,6 +5280,7 @@ def multi_factor_auth_setup(
key_id: nil,
mfa_method_counts: nil,
authenticator_data_flags: nil,
+ attempts: nil,
aaguid: nil,
unknown_transports: nil,
**extra
@@ -5360,6 +5306,7 @@ def multi_factor_auth_setup(
key_id:,
mfa_method_counts:,
authenticator_data_flags:,
+ attempts:,
aaguid:,
unknown_transports:,
**extra,
@@ -5989,6 +5936,24 @@ def piv_cac_login_visited
track_event(:piv_cac_login_visited)
end
+ # User submits prompt to replace PIV/CAC after failing to authenticate due to mismatched subject
+ # @param [Boolean] add_piv_cac_after_2fa User chooses to replace PIV/CAC authenticator
+ def piv_cac_mismatch_submitted(add_piv_cac_after_2fa:, **extra)
+ track_event(:piv_cac_mismatch_submitted, add_piv_cac_after_2fa:, **extra)
+ end
+
+ # User visits prompt to replace PIV/CAC after failing to authenticate due to mismatched subject
+ # @param [Boolean] piv_cac_required Partner requires HSPD12 authentication
+ # @param [Boolean] has_other_authentication_methods User has non-PIV authentication methods
+ def piv_cac_mismatch_visited(piv_cac_required:, has_other_authentication_methods:, **extra)
+ track_event(
+ :piv_cac_mismatch_visited,
+ piv_cac_required:,
+ has_other_authentication_methods:,
+ **extra,
+ )
+ end
+
# @param [String] action what action user made
# Tracks when user submits an action on Piv Cac recommended page
def piv_cac_recommended(action: nil, **extra)
@@ -6009,11 +5974,18 @@ def piv_cac_recommended_visited
# Tracks when user's piv cac setup
# @param [Boolean] in_account_creation_flow Whether user is going through account creation
# @param [Integer] enabled_mfa_methods_count Number of enabled MFA methods on the account
- def piv_cac_setup_visited(in_account_creation_flow:, enabled_mfa_methods_count: nil, **extra)
+ # @param [Integer] attempts number of MFA setup attempts
+ def piv_cac_setup_visited(
+ in_account_creation_flow:,
+ enabled_mfa_methods_count: nil,
+ attempts: nil,
+ **extra
+ )
track_event(
:piv_cac_setup_visited,
in_account_creation_flow:,
enabled_mfa_methods_count:,
+ attempts:,
**extra,
)
end
diff --git a/app/services/flow/base_flow.rb b/app/services/flow/base_flow.rb
index 8bd5a08e8df..18e201ed721 100644
--- a/app/services/flow/base_flow.rb
+++ b/app/services/flow/base_flow.rb
@@ -5,13 +5,11 @@ class BaseFlow
include Failure
attr_accessor :flow_session
- attr_reader :steps, :actions, :current_user, :current_sp, :params, :request, :json,
+ attr_reader :current_user, :current_sp, :params, :request, :json,
:http_status, :controller
- def initialize(controller, steps, actions, session)
+ def initialize(controller, session)
@controller = controller
- @steps = steps.with_indifferent_access
- @actions = actions.with_indifferent_access
@redirect = nil
@json = nil
@flow_session = session
diff --git a/app/services/flow/flow_state_machine.rb b/app/services/flow/flow_state_machine.rb
index 02ca54fdcc3..338e86de78e 100644
--- a/app/services/flow/flow_state_machine.rb
+++ b/app/services/flow/flow_state_machine.rb
@@ -13,7 +13,7 @@ module FlowStateMachine
attr_accessor :flow
def index
- redirect_to_step(next_step)
+ redirect_to idv_in_person_proofing_state_id_url
end
def show
@@ -169,17 +169,13 @@ def flow_finish
redirect_to send(@final_url)
end
- def redirect_to_step(step)
+ def redirect_to_step(_step)
flow_finish and return unless next_step
- redirect_url(step)
+ redirect_url
end
- def redirect_url(step)
- if IdentityConfig.store.in_person_state_id_controller_enabled
- redirect_to idv_in_person_proofing_state_id_url
- else
- redirect_to send(@step_url, step: step)
- end
+ def redirect_url
+ redirect_to idv_in_person_proofing_state_id_url
end
def analytics_properties
diff --git a/app/services/idv/actions/in_person/cancel_update_state_id_action.rb b/app/services/idv/actions/in_person/cancel_update_state_id_action.rb
deleted file mode 100644
index 3dd0e1516d6..00000000000
--- a/app/services/idv/actions/in_person/cancel_update_state_id_action.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module Idv
- module Actions
- module InPerson
- class CancelUpdateStateIdAction < Idv::Steps::DocAuthBaseStep
- def self.analytics_submitted_event
- :idv_in_person_proofing_cancel_update_state_id
- end
-
- def call
- mark_step_complete(:state_id) if flow_session.dig(:pii_from_user, :first_name)
- redirect_to idv_in_person_verify_info_url
- end
- end
- end
- end
-end
diff --git a/app/services/idv/actions/in_person/redo_state_id_action.rb b/app/services/idv/actions/in_person/redo_state_id_action.rb
deleted file mode 100644
index 59d4337e197..00000000000
--- a/app/services/idv/actions/in_person/redo_state_id_action.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module Idv
- module Actions
- module InPerson
- class RedoStateIdAction < Idv::Steps::DocAuthBaseStep
- def self.analytics_submitted_event
- :idv_in_person_proofing_redo_state_id_submitted
- end
-
- def call
- mark_step_incomplete(:state_id)
- end
- end
- end
- end
-end
diff --git a/app/services/idv/analytics_events_enhancer.rb b/app/services/idv/analytics_events_enhancer.rb
index 1dff6c81e89..6f67339f580 100644
--- a/app/services/idv/analytics_events_enhancer.rb
+++ b/app/services/idv/analytics_events_enhancer.rb
@@ -54,12 +54,10 @@ module AnalyticsEventsEnhancer
idv_in_person_prepare_submitted
idv_in_person_prepare_visited
idv_in_person_proofing_address_visited
- idv_in_person_proofing_cancel_update_state_id
idv_in_person_proofing_enrollments_ready_for_status_check_job_completed
idv_in_person_proofing_enrollments_ready_for_status_check_job_ingestion_error
idv_in_person_proofing_enrollments_ready_for_status_check_job_started
idv_in_person_proofing_nontransliterable_characters_submitted
- idv_in_person_proofing_redo_state_id_submitted
idv_in_person_proofing_residential_address_submitted
idv_in_person_proofing_state_id_submitted
idv_in_person_proofing_state_id_visited
diff --git a/app/services/idv/flows/in_person_flow.rb b/app/services/idv/flows/in_person_flow.rb
index 823dfc7c891..1c393f4465f 100644
--- a/app/services/idv/flows/in_person_flow.rb
+++ b/app/services/idv/flows/in_person_flow.rb
@@ -5,15 +5,6 @@ module Flows
class InPersonFlow < Flow::BaseFlow
attr_reader :idv_session # this is used by DocAuthBaseStep
- STEPS = {
- state_id: Idv::Steps::InPerson::StateIdStep, # info from state id
- }.freeze
-
- ACTIONS = {
- cancel_update_state_id: Idv::Actions::InPerson::CancelUpdateStateIdAction,
- redo_state_id: Idv::Actions::InPerson::RedoStateIdAction,
- }.freeze
-
STEP_INDICATOR_STEPS = [
{ name: :find_a_post_office },
{ name: :verify_info },
@@ -32,7 +23,7 @@ class InPersonFlow < Flow::BaseFlow
def initialize(controller, session, name)
@idv_session = self.class.session_idv(session)
- super(controller, STEPS, ACTIONS, session[name])
+ super(controller, session[name])
@flow_session ||= {}
@flow_session[:pii_from_user] ||= { uuid: current_user.uuid }
# there may be data in @idv_session to copy to @flow_session
diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb
index bf52fb4b991..ea2db47c62c 100644
--- a/app/services/idv/session.rb
+++ b/app/services/idv/session.rb
@@ -222,8 +222,6 @@ def has_pii_from_user_in_flow_session?
def invalidate_in_person_pii_from_user!
if has_pii_from_user_in_flow_session?
user_session['idv/in_person'][:pii_from_user] = nil
- # Mark the FSM step as incomplete so that it can be re-entered.
- user_session['idv/in_person'].delete('Idv::Steps::InPerson::StateIdStep')
end
end
diff --git a/app/services/idv/steps/in_person/state_id_step.rb b/app/services/idv/steps/in_person/state_id_step.rb
deleted file mode 100644
index 23a2f15fbf8..00000000000
--- a/app/services/idv/steps/in_person/state_id_step.rb
+++ /dev/null
@@ -1,127 +0,0 @@
-# frozen_string_literal: true
-
-module Idv
- module Steps
- module InPerson
- class StateIdStep < DocAuthBaseStep
- STEP_INDICATOR_STEP = :verify_info
-
- def self.analytics_visited_event
- :idv_in_person_proofing_state_id_visited
- end
-
- def self.analytics_submitted_event
- :idv_in_person_proofing_state_id_submitted
- end
-
- def call
- pii_from_user = flow_session[:pii_from_user]
- initial_state_of_same_address_as_id = flow_session[:pii_from_user][:same_address_as_id]
- Idv::StateIdForm::ATTRIBUTES.each do |attr|
- flow_session[:pii_from_user][attr] = flow_params[attr]
- end
- # Accept Date of Birth from both memorable date and input date components
- formatted_dob = MemorableDateComponent.extract_date_param flow_params&.[](:dob)
- pii_from_user[:dob] = formatted_dob if formatted_dob
-
- if pii_from_user[:same_address_as_id] == 'true'
- copy_state_id_address_to_residential_address(pii_from_user)
- redirect_to idv_in_person_ssn_url
- end
-
- if initial_state_of_same_address_as_id == 'true' &&
- pii_from_user[:same_address_as_id] == 'false'
- clear_residential_address(pii_from_user)
- end
-
- redirect_to idv_in_person_verify_info_url if updating_state_id?
-
- if pii_from_user[:same_address_as_id] == 'false'
- redirect_to idv_in_person_address_url
- end
- end
-
- def extra_view_variables
- {
- form:,
- pii:,
- parsed_dob:,
- updating_state_id: updating_state_id?,
- }
- end
-
- private
-
- def clear_residential_address(pii_from_user)
- pii_from_user.delete(:address1)
- pii_from_user.delete(:address2)
- pii_from_user.delete(:city)
- pii_from_user.delete(:state)
- pii_from_user.delete(:zipcode)
- end
-
- def copy_state_id_address_to_residential_address(pii_from_user)
- pii_from_user[:address1] = flow_params[:identity_doc_address1]
- pii_from_user[:address2] = flow_params[:identity_doc_address2]
- pii_from_user[:city] = flow_params[:identity_doc_city]
- pii_from_user[:state] = flow_params[:identity_doc_address_state]
- pii_from_user[:zipcode] = flow_params[:identity_doc_zipcode]
- end
-
- def updating_state_id?
- flow_session[:pii_from_user].has_key?(:first_name)
- end
-
- def parsed_dob
- form_dob = pii[:dob]
- if form_dob.instance_of?(String)
- dob_str = form_dob
- elsif form_dob.instance_of?(Hash)
- dob_str = MemorableDateComponent.extract_date_param(form_dob)
- end
- Date.parse(dob_str) unless dob_str.nil?
- rescue StandardError
- # Catch date parsing errors
- end
-
- def pii
- data = flow_session[:pii_from_user]
- if params.has_key?(:identity_doc) || params.has_key?(:state_id)
- data = data.merge(flow_params)
- end
- data.deep_symbolize_keys
- end
-
- def flow_params
- if params.dig(:identity_doc).present?
- # Transform the top-level params key to accept the renamed form
- # for autofill handling workaround
- params[:state_id] = params.delete(:identity_doc)
-
- # Rename nested id_number to state_id_number
- if params[:state_id][:id_number].present?
- params[:state_id][:state_id_number] = params[:state_id].delete(:id_number)
- end
- end
-
- params.require(:state_id).permit(
- *Idv::StateIdForm::ATTRIBUTES,
- dob: [
- :month,
- :day,
- :year,
- ],
- )
- end
-
- def form
- @form ||= Idv::StateIdForm.new(current_user)
- end
-
- def form_submit
- form.submit(flow_params)
- end
- end
- end
- end
-end
diff --git a/app/views/idv/in_person/state_id.html.erb b/app/views/idv/in_person/state_id.html.erb
deleted file mode 100644
index 5c414de16bd..00000000000
--- a/app/views/idv/in_person/state_id.html.erb
+++ /dev/null
@@ -1,244 +0,0 @@
-<% self.title = t('titles.doc_auth.verify') %>
-
-<% if updating_state_id %>
- <%= render PageHeadingComponent.new.with_content(t('in_person_proofing.headings.update_state_id')) %>
-<% else %>
- <%= render PageHeadingComponent.new.with_content(t('in_person_proofing.headings.state_id_milestone_2')) %>
-<% end %>
-
-
- <%= t('in_person_proofing.body.state_id.info_html') %>
-
-
-<%= render AlertComponent.new(
- type: :info,
- class: 'margin-bottom-4',
- text_tag: 'div',
- ) do %>
- <%= t('in_person_proofing.body.state_id.alert_message') %>
-
- <% t('in_person_proofing.body.state_id.id_types').each do | id_type | %>
-
- <%= id_type %>
-
- <% end %>
-
-
- <%= t('in_person_proofing.body.state_id.questions') %>
- <%= link_to(
- help_center_redirect_url(
- category: 'verify-your-identity',
- article: 'accepted-identification-documents',
- ),
- class: 'display-inline',
- ) do %>
- <%= t('in_person_proofing.body.state_id.learn_more_link') %>
- <% end %>
-
-<% end %>
-
-<%= simple_form_for form,
- as: 'identity_doc', # Renaming form as a workaround for aggressive browser autofill assumptions
- url: url_for,
- method: 'put',
- html: { class: 'margin-y-5' } do |f| %>
-
-
- <%= render ValidatedFieldComponent.new(
- name: :first_name,
- form: f,
- input_html: { value: pii[:first_name] },
- label: t('in_person_proofing.form.state_id.first_name'),
- label_html: { class: 'usa-label' },
- maxlength: 255,
- required: true,
- ) %>
-
-
-
- <%= render ValidatedFieldComponent.new(
- name: :last_name,
- form: f,
- input_html: { value: pii[:last_name] },
- label: t('in_person_proofing.form.state_id.last_name'),
- label_html: { class: 'usa-label' },
- maxlength: 255,
- required: true,
- ) %>
-
-
-
- <%= render MemorableDateComponent.new(
- content_tag: 'memorable-date',
- name: :dob,
- day: parsed_dob&.day,
- month: parsed_dob&.month,
- year: parsed_dob&.year,
- required: true,
- min: '1900-01-01',
- max: Time.zone.today,
- hint: t('in_person_proofing.form.state_id.dob_hint'),
- label: t('in_person_proofing.form.state_id.dob'),
- form: f,
- error_messages: {
- missing_month_day_year: t('in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.missing_month_day_year'),
- range_overflow: t('in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.range_overflow'),
- },
- range_errors: [
- {
- max: Time.zone.today - 13.years,
- message: t(
- 'in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.range_min_age',
- app_name: APP_NAME,
- ),
- },
- ],
- )
- %>
-
-
-
- <%= render ValidatedFieldComponent.new(
- name: :state_id_jurisdiction,
- collection: @presenter.usps_states_territories,
- form: f,
- hint: t('in_person_proofing.form.state_id.state_id_jurisdiction_hint'),
- input_html: { class: 'jurisdiction-state-selector' },
- label: t('in_person_proofing.form.state_id.state_id_jurisdiction'),
- label_html: { class: 'usa-label' },
- prompt: t('in_person_proofing.form.state_id.state_id_jurisdiction_prompt'),
- required: true,
- selected: pii[:state_id_jurisdiction],
- ) %>
-
-
- <% state_id_number_hint_default = capture do %>
- <%= t('in_person_proofing.form.state_id.state_id_number_hint') %>
- <% [
- [t('in_person_proofing.form.state_id.state_id_number_hint_spaces'), ' '],
- [t('in_person_proofing.form.state_id.state_id_number_hint_forward_slashes'), '/'],
- [t('in_person_proofing.form.state_id.state_id_number_hint_asterisks'), '*'],
- [t('in_person_proofing.form.state_id.state_id_number_hint_dashes'), '-', true],
- ].each do |text, symbol, last| %>
- <%= text %><%= ',' if !last %>
- <%= symbol %>
- <% end %>
- <% end %>
-
- <% state_id_number_hint = capture do %>
- <% [
- [:default, state_id_number_hint_default],
- ['FL', t('in_person_proofing.form.state_id.state_id_number_florida_hint_html')],
- ['TX', t('in_person_proofing.form.state_id.state_id_number_texas_hint')],
- ].each do |state, hint| %>
- <%= content_tag(
- :span,
- hint,
- class: state == :default ? nil : 'display-none',
- data: { state: },
- ) %>
- <% end %>
- <% end %>
-
- <%= render ValidatedFieldComponent.new(
- name: :id_number, # Renaming field as a workaround for aggressive browser autofill assumptions
- form: f,
- hint: state_id_number_hint,
- hint_html: { class: ['jurisdiction-extras'] },
- input_html: { value: pii[:state_id_number] },
- label: t('in_person_proofing.form.state_id.state_id_number'),
- label_html: { class: 'usa-label' },
- maxlength: 255,
- required: true,
- ) %>
-
-
- <%= t('in_person_proofing.headings.id_address') %>
- <%= render ValidatedFieldComponent.new(
- name: :identity_doc_address_state,
- collection: @presenter.usps_states_territories,
- form: f,
- input_html: { class: 'address-state-selector' },
- label: t('in_person_proofing.form.state_id.identity_doc_address_state'),
- label_html: { class: 'usa-label' },
- prompt: t('in_person_proofing.form.state_id.identity_doc_address_state_prompt'),
- required: true,
- selected: pii[:identity_doc_address_state],
- ) %>
- <%= render ValidatedFieldComponent.new(
- name: :identity_doc_address1,
- form: f,
- hint_html: { class: ['display-none', 'puerto-rico-extras'] },
- hint: t('in_person_proofing.form.state_id.address1_hint'),
- input_html: { value: pii[:identity_doc_address1] },
- label: t('in_person_proofing.form.state_id.address1'),
- label_html: { class: 'usa-label' },
- maxlength: 255,
- required: true,
- ) %>
-
- <%= render ValidatedFieldComponent.new(
- name: :identity_doc_address2,
- form: f,
- hint: t('in_person_proofing.form.state_id.address2_hint'),
- hint_html: { class: ['display-none', 'puerto-rico-extras'] },
- input_html: { value: pii[:identity_doc_address2] },
- label: t('in_person_proofing.form.state_id.address2'),
- label_html: { class: 'usa-label' },
- maxlength: 255,
- required: false,
- ) %>
-
- <%= render ValidatedFieldComponent.new(
- name: :identity_doc_city,
- form: f,
- input_html: { value: pii[:identity_doc_city] },
- label: t('in_person_proofing.form.state_id.city'),
- label_html: { class: 'usa-label' },
- maxlength: 255,
- required: true,
- ) %>
-
- <%# using :tel for mobile numeric keypad %>
- <%= render ValidatedFieldComponent.new(
- as: :tel,
- error_messages: { patternMismatch: t('idv.errors.pattern_mismatch.zipcode') },
- form: f,
- input_html: { value: pii[:identity_doc_zipcode], class: 'zipcode' },
- label: t('in_person_proofing.form.state_id.zipcode'),
- label_html: { class: 'usa-label' },
- name: :identity_doc_zipcode,
- pattern: '\d{5}([\-]\d{4})?',
- required: true,
- ) %>
-
- <%= render ValidatedFieldComponent.new(
- as: :radio_buttons,
- checked: pii[:same_address_as_id],
- collection: [
- [t('in_person_proofing.form.state_id.same_address_as_id_yes'), true],
- [t('in_person_proofing.form.state_id.same_address_as_id_no'), false],
- ],
- form: f,
- label: t('in_person_proofing.form.state_id.same_address_as_id'),
- legend_html: { class: 'h2' },
- name: :same_address_as_id,
- required: true,
- wrapper: :uswds_radio_buttons,
- ) %>
-
- <%= f.submit do %>
- <% if updating_state_id %>
- <%= t('forms.buttons.submit.update') %>
- <% else %>
- <%= t('forms.buttons.continue') %>
- <% end %>
- <% end %>
-<% end %>
-
-<% if updating_state_id %>
- <%= render 'idv/shared/back', action: 'cancel_update_state_id' %>
-<% else %>
- <%= render 'idv/doc_auth/cancel', step: 'state_id' %>
-<% end %>
-<%= javascript_packs_tag_once('formatted-fields', 'state-guidance') %>
diff --git a/app/views/idv/in_person/verify_info/show.html.erb b/app/views/idv/in_person/verify_info/show.html.erb
index 5ce62008459..baf8157e7d9 100644
--- a/app/views/idv/in_person/verify_info/show.html.erb
+++ b/app/views/idv/in_person/verify_info/show.html.erb
@@ -72,9 +72,8 @@ locals:
- <%= button_to(
- idv_in_person_step_url(step: :redo_state_id),
- method: :put,
+ <%= link_to(
+ idv_in_person_proofing_state_id_url,
class: 'usa-button usa-button--unstyled padding-y-1',
'aria-label': t('idv.buttons.change_state_id_label'),
) { t('idv.buttons.change_label') } %>
diff --git a/app/views/two_factor_authentication/options/index.html.erb b/app/views/two_factor_authentication/options/index.html.erb
index d17c9914614..9a55980a0ee 100644
--- a/app/views/two_factor_authentication/options/index.html.erb
+++ b/app/views/two_factor_authentication/options/index.html.erb
@@ -2,6 +2,13 @@
<%= render(VendorOutageAlertComponent.new(vendors: [:sms, :voice])) %>
+<% if @presenter.add_piv_cac_after_2fa? %>
+ <%= render AlertComponent.new(
+ type: :info,
+ class: 'margin-bottom-4',
+ ).with_content(t('two_factor_authentication.piv_cac_mismatch.2fa_before_add')) %>
+<% end %>
+
<%= render PageHeadingComponent.new.with_content(@presenter.heading) %>
diff --git a/app/views/two_factor_authentication/piv_cac_mismatch/show.html.erb b/app/views/two_factor_authentication/piv_cac_mismatch/show.html.erb
new file mode 100644
index 00000000000..eee6ddd11f6
--- /dev/null
+++ b/app/views/two_factor_authentication/piv_cac_mismatch/show.html.erb
@@ -0,0 +1,38 @@
+<% self.title = t('two_factor_authentication.piv_cac_mismatch.title') %>
+
+<%= render PageHeadingComponent.new.with_content(t('two_factor_authentication.piv_cac_mismatch.title')) %>
+
+<% if @has_other_authentication_methods %>
+
<%= t('two_factor_authentication.piv_cac_mismatch.instructions') %>
+
+ <%= render ButtonComponent.new(
+ url: login_two_factor_piv_cac_mismatch_url,
+ method: :post,
+ params: { add_piv_cac_after_2fa: 'true' },
+ big: true,
+ wide: true,
+ class: 'display-block margin-top-5',
+ ).with_content(t('two_factor_authentication.piv_cac_mismatch.cta')) %>
+
+ <% if !@piv_cac_required %>
+ <%= render ButtonComponent.new(
+ url: login_two_factor_piv_cac_mismatch_url,
+ method: :post,
+ unstyled: true,
+ class: 'display-block margin-top-2',
+ ).with_content(t('two_factor_authentication.piv_cac_mismatch.skip')) %>
+ <% end %>
+<% else %>
+
<%= t('two_factor_authentication.piv_cac_mismatch.instructions_no_other_method', app_name: APP_NAME) %>
+
+ <%= render ButtonComponent.new(
+ url: account_reset_recovery_options_url,
+ big: true,
+ wide: true,
+ class: 'display-inline-block margin-top-3',
+ ).with_content(t('two_factor_authentication.piv_cac_mismatch.delete_account')) %>
+<% end %>
+
+<%= render PageFooterComponent.new do %>
+ <%= link_to t('links.cancel'), sign_out_url %>
+<% end %>
diff --git a/app/views/users/piv_cac_authentication_setup/new.html.erb b/app/views/users/piv_cac_authentication_setup/new.html.erb
index 172c77960dd..1807862381e 100644
--- a/app/views/users/piv_cac_authentication_setup/new.html.erb
+++ b/app/views/users/piv_cac_authentication_setup/new.html.erb
@@ -31,8 +31,22 @@
<% end %>
<% end %>
- <%= f.submit t('forms.piv_cac_setup.submit'), class: 'display-block margin-y-5' %>
+ <%= f.submit t('forms.piv_cac_setup.submit'), class: 'display-block margin-top-5 margin-bottom-2' %>
<% end %>
+<% if user_session[:add_piv_cac_after_2fa] && !@piv_cac_required %>
+ <%= render ButtonComponent.new(
+ url: submit_new_piv_cac_url,
+ method: :post,
+ params: { skip: 'true' },
+ unstyled: true,
+ ).with_content(t('mfa.skip')) %>
+<% end %>
-<%= render 'shared/cancel_or_back_to_options' %>
+<% if user_session[:add_piv_cac_after_2fa] %>
+ <%= render PageFooterComponent.new do %>
+ <%= link_to t('links.cancel'), sign_out_path %>
+ <% end %>
+<% else %>
+ <%= render 'shared/cancel_or_back_to_options' %>
+<% end %>
diff --git a/config/application.rb b/config/application.rb
index 5b544fd382d..64f212ce46f 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -59,7 +59,7 @@ class Application < Rails::Application
end
end
- config.load_defaults '7.1'
+ config.load_defaults '7.2'
config.active_record.belongs_to_required_by_default = false
config.active_job.queue_adapter = :good_job
@@ -157,9 +157,7 @@ class Application < Rails::Application
allow do
origins IdentityCors.allowed_origins_static_sites
resource '/api/country-support', headers: :any, methods: [:get]
- if Identity::Hostdata.config.in_person_public_address_search_enabled
- resource '/api/usps_locations', headers: :any, methods: %i[post options]
- end
+ resource '/api/usps_locations', headers: :any, methods: %i[post options]
end
end
diff --git a/config/application.yml.default b/config/application.yml.default
index 5ef389edbf4..ada79aca234 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -184,10 +184,8 @@ in_person_outage_message_enabled: false
in_person_proofing_enabled: false
in_person_proofing_enforce_tmx: false
in_person_proofing_opt_in_enabled: false
-in_person_public_address_search_enabled: false
in_person_results_delay_in_hours: 1
in_person_send_proofing_notifications_enabled: false
-in_person_state_id_controller_enabled: false
in_person_stop_expiring_enrollments: false
invalid_gpo_confirmation_zipcode: '00001'
# LexisNexis #####################################################
@@ -543,7 +541,6 @@ test:
hmac_fingerprinter_key: a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c
hmac_fingerprinter_key_queue: '["old-key-one", "old-key-two"]'
identity_pki_disabled: true
- in_person_state_id_controller_enabled: true
lexisnexis_trueid_account_id: 'test_account'
lockout_period_in_minutes: 5
logins_per_email_and_ip_limit: 2
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 5097c9f195f..35df3e3e9d2 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -27,7 +27,7 @@
if IdentityConfig.store.rails_mailer_previews_enabled
config.action_mailer.show_previews = true
- config.action_mailer.preview_path = Rails.root.join('spec/mailers/previews')
+ config.action_mailer.preview_paths = [Rails.root.join('spec/mailers/previews')]
end
routes.default_url_options[:protocol] = :https
diff --git a/config/locales/en.yml b/config/locales/en.yml
index ebd7685fc38..c4e56d5d3c2 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1716,6 +1716,13 @@ two_factor_authentication.phone_verification.troubleshooting.code_not_received:
two_factor_authentication.phone.delete.failure: Unable to remove your phone.
two_factor_authentication.phone.delete.success: Your phone has been removed.
two_factor_authentication.piv_cac_header_text: Insert your government employee ID
+two_factor_authentication.piv_cac_mismatch.2fa_before_add: You need to authenticate with another method before adding your PIV/CAC.
+two_factor_authentication.piv_cac_mismatch.cta: Authenticate and add PIV/CAC
+two_factor_authentication.piv_cac_mismatch.delete_account: Delete your account
+two_factor_authentication.piv_cac_mismatch.instructions: Click “Authenticate and add PIV/CAC” below to authenticate with another method before adding this PIV/CAC to your account.
+two_factor_authentication.piv_cac_mismatch.instructions_no_other_method: If you were reissued your PIV/CAC, you will need to delete your %{app_name} account and create a new account to use your reissued PIV/CAC.
+two_factor_authentication.piv_cac_mismatch.skip: Skip adding PIV/CAC
+two_factor_authentication.piv_cac_mismatch.title: This government employee ID is not connected to your account
two_factor_authentication.piv_cac_upsell.add_piv: Add PIV/CAC card
two_factor_authentication.piv_cac_upsell.choose_other_method: Choose other methods instead
two_factor_authentication.piv_cac_upsell.explain: This will improve your account security and let you skip entering your email and password when signing in.
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 3497461ffe1..5ab154e9ebd 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -1728,6 +1728,13 @@ two_factor_authentication.phone_verification.troubleshooting.code_not_received:
two_factor_authentication.phone.delete.failure: No se puede eliminar su teléfono.
two_factor_authentication.phone.delete.success: Su teléfono fue eliminado.
two_factor_authentication.piv_cac_header_text: Inserte su identificación de empleado del gobierno
+two_factor_authentication.piv_cac_mismatch.2fa_before_add: Debe realizar la autenticación con otro método antes de añadir su tarjeta PIV o CAC.
+two_factor_authentication.piv_cac_mismatch.cta: Autenticar y añadir tarjeta PIV o CAC
+two_factor_authentication.piv_cac_mismatch.delete_account: Eliminar su cuenta
+two_factor_authentication.piv_cac_mismatch.instructions: Haga clic en “Autenticar y añadir tarjeta PIV o CAC” más abajo para autenticar con otro método antes de añadir esta tarjeta PIV o CAC a su cuenta.
+two_factor_authentication.piv_cac_mismatch.instructions_no_other_method: Si se le emitió una nueva tarjeta PIV o CAC, para poder usarla, deberá eliminar su cuenta de %{app_name} y crear una cuenta nueva.
+two_factor_authentication.piv_cac_mismatch.skip: Saltar añadir tarjeta PIV o CAC
+two_factor_authentication.piv_cac_mismatch.title: Esta tarjeta de identificación de empleado del gobierno no está conectada a su cuenta
two_factor_authentication.piv_cac_upsell.add_piv: Agregar tarjeta PIV/CAC
two_factor_authentication.piv_cac_upsell.choose_other_method: Elegir otros métodos
two_factor_authentication.piv_cac_upsell.explain: Esto hará que su cuenta sea más segura y no tendrá que ingresar su correo electrónico ni su contraseña cuando inicie sesión.
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index e49b209a072..1c7fd9cb6e1 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -1716,6 +1716,13 @@ two_factor_authentication.phone_verification.troubleshooting.code_not_received:
two_factor_authentication.phone.delete.failure: Impossible de supprimer votre téléphone.
two_factor_authentication.phone.delete.success: Votre téléphone a été supprimé.
two_factor_authentication.piv_cac_header_text: Insérer votre carte d’employé fédéral
+two_factor_authentication.piv_cac_mismatch.2fa_before_add: Vous devez vous authentifier à l’aide d’une autre méthode avant d’ajouter votre carte PIV/CAC.
+two_factor_authentication.piv_cac_mismatch.cta: S’authentifier et ajouter une carte PIV/CAC
+two_factor_authentication.piv_cac_mismatch.delete_account: Supprimer votre compte
+two_factor_authentication.piv_cac_mismatch.instructions: Cliquez sur « S’authentifier et ajouter une carte PIV/CAC » ci-dessous pour vous authentifier au moyen d’une autre méthode avant d’ajouter cette carte PIV/CAC à votre compte.
+two_factor_authentication.piv_cac_mismatch.instructions_no_other_method: Si l’on vous a à nouveau délivré une carte PIV/CAC, vous devez supprimer votre compte %{app_name} et en créer un nouveau pour l’utiliser.
+two_factor_authentication.piv_cac_mismatch.skip: Sauter l’ajout de carte PIV/CAC
+two_factor_authentication.piv_cac_mismatch.title: Cette carte d’employé fédéral n’est pas associée à votre compte.
two_factor_authentication.piv_cac_upsell.add_piv: Ajouter une carte PIV/CAC
two_factor_authentication.piv_cac_upsell.choose_other_method: Choisir plutôt d’autres méthodes
two_factor_authentication.piv_cac_upsell.explain: Ceci permettra de renforcer la sécurité de votre compte et de sauter l’étape de saisie de votre e-mail et mot de passe quand vous vous connecterez.
diff --git a/config/locales/zh.yml b/config/locales/zh.yml
index bcc3a9451e0..d7e911f2dc5 100644
--- a/config/locales/zh.yml
+++ b/config/locales/zh.yml
@@ -1729,6 +1729,13 @@ two_factor_authentication.phone_verification.troubleshooting.code_not_received:
two_factor_authentication.phone.delete.failure: 无法去掉你的电话。
two_factor_authentication.phone.delete.success: 你的电话已被去掉。
two_factor_authentication.piv_cac_header_text: 插入您的政府雇员ID
+two_factor_authentication.piv_cac_mismatch.2fa_before_add: 添加你的PIV/CAC之前,你需要使用另外一种方法进行身份证实。
+two_factor_authentication.piv_cac_mismatch.cta: 进行身份证实并添加PIV/CAC
+two_factor_authentication.piv_cac_mismatch.delete_account: 删除你的帐户
+two_factor_authentication.piv_cac_mismatch.instructions: 点击下边的“进行身份证实并添加PIV/CAC”,以在将该PIV/CAC添加到你账户之前使用另外一种方法进行身份证实。
+two_factor_authentication.piv_cac_mismatch.instructions_no_other_method: 如果你的PIV/CAV是重新颁发的,你需要删除自己的%{app_name}帐户,并使用重新颁发的PIV/CAC 设立一个新账户。
+two_factor_authentication.piv_cac_mismatch.skip: 跳过添加 PIV/CAC
+two_factor_authentication.piv_cac_mismatch.title: 该政府雇员身份证件与你的账户没有连接起来
two_factor_authentication.piv_cac_upsell.add_piv: 添加 PIV/CAC 卡
two_factor_authentication.piv_cac_upsell.choose_other_method: 选择其他方法
two_factor_authentication.piv_cac_upsell.explain: 这将改善你账户安全,而且你在登录时无需再输入电邮和密码。
diff --git a/config/routes.rb b/config/routes.rb
index 6952004af90..0a903eab6c9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -119,6 +119,9 @@
get '/login/two_factor/options' => 'two_factor_authentication/options#index'
post '/login/two_factor/options' => 'two_factor_authentication/options#create'
+ get '/login/two_factor/piv_cac_mismatch' => 'two_factor_authentication/piv_cac_mismatch#show'
+ post '/login/two_factor/piv_cac_mismatch' => 'two_factor_authentication/piv_cac_mismatch#create'
+
get '/login/two_factor/authenticator' => 'two_factor_authentication/totp_verification#show'
post '/login/two_factor/authenticator' => 'two_factor_authentication/totp_verification#create'
get '/login/two_factor/personal_key' => 'two_factor_authentication/personal_key_verification#show'
diff --git a/db/primary_migrate/20241023191918_add_socure_docv_capture_app_url_to_document_capture_sessions_w_comment.rb b/db/primary_migrate/20241023191918_add_socure_docv_capture_app_url_to_document_capture_sessions_w_comment.rb
new file mode 100644
index 00000000000..6fe9739d4af
--- /dev/null
+++ b/db/primary_migrate/20241023191918_add_socure_docv_capture_app_url_to_document_capture_sessions_w_comment.rb
@@ -0,0 +1,5 @@
+class AddSocureDocvCaptureAppUrlToDocumentCaptureSessionsWComment < ActiveRecord::Migration[7.2]
+ def change
+ add_column :document_capture_sessions, :socure_docv_capture_app_url, :string, comment: 'sensitive=false'
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a20b704ba51..cbb37d407b2 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.1].define(version: 2024_10_17_153042) do
+ActiveRecord::Schema[7.2].define(version: 2024_10_23_191918) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "pg_stat_statements"
@@ -192,6 +192,7 @@
t.boolean "ocr_confirmation_pending", default: false, comment: "sensitive=false"
t.string "last_doc_auth_result", comment: "sensitive=false"
t.string "socure_docv_transaction_token", comment: "sensitive=false"
+ t.string "socure_docv_capture_app_url", comment: "sensitive=false"
t.index ["result_id"], name: "index_document_capture_sessions_on_result_id"
t.index ["user_id"], name: "index_document_capture_sessions_on_user_id"
t.index ["uuid"], name: "index_document_capture_sessions_on_uuid"
diff --git a/db/worker_jobs_schema.rb b/db/worker_jobs_schema.rb
index 3398c6bca59..3e6fa136f59 100644
--- a/db/worker_jobs_schema.rb
+++ b/db/worker_jobs_schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2024_10_21_192437) do
+ActiveRecord::Schema[7.2].define(version: 2024_10_21_192437) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -102,5 +102,4 @@
t.index ["queue_name", "scheduled_at"], name: "index_good_jobs_on_queue_name_and_scheduled_at", where: "(finished_at IS NULL)"
t.index ["scheduled_at"], name: "index_good_jobs_on_scheduled_at", where: "(finished_at IS NULL)"
end
-
end
diff --git a/docs/frontend.md b/docs/frontend.md
index 48dd9a01142..80babf8ff59 100644
--- a/docs/frontend.md
+++ b/docs/frontend.md
@@ -233,6 +233,10 @@ For example, consider a **Password Input** component:
- A web component would be named `PasswordInputElement`
- A web components file would be named `app/javascript/packages/password-input/password-input-element.ts`
+#### Graphical Assets
+
+Web graphic assets like images, GIFs, and videos are artifacts authored in other tools. As such, there is no need to keep multiple variants of an asset (e.g., SVG and PNG) in the repository if they are not in use.
+
## Testing
### Stylelint
diff --git a/lib/identity_config.rb b/lib/identity_config.rb
index 6e0b04b2a07..b64e7e5fa77 100644
--- a/lib/identity_config.rb
+++ b/lib/identity_config.rb
@@ -199,10 +199,8 @@ def self.store
config.add(:in_person_proofing_enabled, type: :boolean)
config.add(:in_person_proofing_enforce_tmx, type: :boolean)
config.add(:in_person_proofing_opt_in_enabled, type: :boolean)
- config.add(:in_person_public_address_search_enabled, type: :boolean)
config.add(:in_person_results_delay_in_hours, type: :integer)
config.add(:in_person_send_proofing_notifications_enabled, type: :boolean)
- config.add(:in_person_state_id_controller_enabled, type: :boolean)
config.add(:in_person_stop_expiring_enrollments, type: :boolean)
config.add(:invalid_gpo_confirmation_zipcode, type: :string)
config.add(:lexisnexis_account_id, type: :string)
diff --git a/lib/reporting/identity_verification_report.rb b/lib/reporting/identity_verification_report.rb
index 8ee8fbb3e77..089a9d8f27a 100644
--- a/lib/reporting/identity_verification_report.rb
+++ b/lib/reporting/identity_verification_report.rb
@@ -53,6 +53,17 @@ module Results
# rubocop:enable Layout/LineLength
end
+ # Because historically fraud-related events were not tagged with SP data,
+ # we need pull these out-of-band events *even if* the are marked as
+ # pending fraud review. This allows us to attribute untagged fraud-related
+ # events (by matching on user_id). We filter these events for counting
+ # purposes, though.
+ EVENTS_TO_IGNORE_IF_FRAUD_REVIEW_PENDING = [
+ Events::GPO_VERIFICATION_SUBMITTED,
+ Events::GPO_VERIFICATION_SUBMITTED_OLD,
+ Events::USPS_ENROLLMENT_STATUS_UPDATED,
+ ].to_set.freeze
+
# @param [Array
] issuers
# @param [Range] date
def initialize(
@@ -218,11 +229,43 @@ def usps_enrollment_status_updated
data[Events::USPS_ENROLLMENT_STATUS_UPDATED].count
end
+ def passed_fraud_review_users
+ # Fraud review events may not be tagged with the issuer.
+ # When we are filtering by SP, we only count fraud review events where
+ # there is another event for the user in the data that _is_ tagged
+ # with the issuer.
+
+ users = data[Events::FRAUD_REVIEW_PASSED]
+
+ return users if issuers.nil? || issuers.empty?
+
+ users_with_events_for_any_issuer =
+ issuers.each_with_object(Set.new) do |issuer, accumulated_users|
+ accumulated_users.merge(data[sp_key(issuer)])
+ end
+
+ users & users_with_events_for_any_issuer
+ end
+
+ def did_not_pass_fraud_review_users
+ result = (
+ data[Events::FRAUD_REVIEW_REJECT_AUTOMATIC] +
+ data[Events::FRAUD_REVIEW_REJECT_MANUAL]
+ )
+
+ issuers&.each do |issuer|
+ users_with_events_for_issuer = data[sp_key(issuer)]
+ result &= users_with_events_for_issuer
+ end
+
+ result
+ end
+
def successfully_verified_users
@successfully_verified_users ||= (
data[Results::IDV_FINAL_RESOLUTION_VERIFIED] +
data[Events::USPS_ENROLLMENT_STATUS_UPDATED] +
- data[Events::FRAUD_REVIEW_PASSED] +
+ passed_fraud_review_users +
data[Events::GPO_VERIFICATION_SUBMITTED] +
data[Events::GPO_VERIFICATION_SUBMITTED_OLD]
).count
@@ -252,11 +295,11 @@ def idv_doc_auth_rejected
end
def idv_fraud_rejected
- (data[Events::FRAUD_REVIEW_REJECT_AUTOMATIC] + data[Events::FRAUD_REVIEW_REJECT_MANUAL]).count
+ did_not_pass_fraud_review_users.count
end
def fraud_review_passed
- data[Events::FRAUD_REVIEW_PASSED].count
+ passed_fraud_review_users.count
end
def verified_user_count
@@ -272,7 +315,7 @@ def verified_user_count
# @return [Hash>]
def data
@data ||= begin
- event_users = Hash.new do |h, event_name|
+ users = Hash.new do |h, event_name|
h[event_name] = Set.new
end
@@ -282,39 +325,43 @@ def data
event = row['name']
user_id = row['user_id']
success = row['success']
+ gpo_verification_pending = row['gpo_verification_pending'] == '1'
+ in_person_verification_pending = row['in_person_verification_pending'] == '1'
+ fraud_review_pending = row['fraud_review_pending'] == '1'
- event_users[event] << user_id
+ ignore_event_for_user =
+ fraud_review_pending &&
+ EVENTS_TO_IGNORE_IF_FRAUD_REVIEW_PENDING.include?(event)
+
+ users[event] << user_id unless ignore_event_for_user
+ users[sp_key(row['service_provider'])] << user_id if row['service_provider'].present?
case event
when Events::IDV_FINAL_RESOLUTION
- event_users[Results::IDV_FINAL_RESOLUTION_VERIFIED] << user_id if row['identity_verified'] == '1'
-
- gpo_verification_pending = row['gpo_verification_pending'] == '1'
- in_person_verification_pending = row['in_person_verification_pending'] == '1'
- fraud_review_pending = row['fraud_review_pending'] == '1'
+ users[Results::IDV_FINAL_RESOLUTION_VERIFIED] << user_id if row['identity_verified'] == '1'
if !gpo_verification_pending && !in_person_verification_pending
- event_users[Results::IDV_FINAL_RESOLUTION_FRAUD_REVIEW] << user_id if fraud_review_pending
+ users[Results::IDV_FINAL_RESOLUTION_FRAUD_REVIEW] << user_id if fraud_review_pending
elsif gpo_verification_pending && !in_person_verification_pending
- event_users[Results::IDV_FINAL_RESOLUTION_GPO] << user_id if !fraud_review_pending
- event_users[Results::IDV_FINAL_RESOLUTION_GPO_FRAUD_REVIEW] << user_id if fraud_review_pending
+ users[Results::IDV_FINAL_RESOLUTION_GPO] << user_id if !fraud_review_pending
+ users[Results::IDV_FINAL_RESOLUTION_GPO_FRAUD_REVIEW] << user_id if fraud_review_pending
elsif !gpo_verification_pending && in_person_verification_pending
- event_users[Results::IDV_FINAL_RESOLUTION_IN_PERSON] << user_id if !fraud_review_pending
- event_users[Results::IDV_FINAL_RESOLUTION_IN_PERSON_FRAUD_REVIEW] << user_id if fraud_review_pending
+ users[Results::IDV_FINAL_RESOLUTION_IN_PERSON] << user_id if !fraud_review_pending
+ users[Results::IDV_FINAL_RESOLUTION_IN_PERSON_FRAUD_REVIEW] << user_id if fraud_review_pending
elsif gpo_verification_pending && in_person_verification_pending
- event_users[Results::IDV_FINAL_RESOLUTION_GPO_IN_PERSON] << user_id if !fraud_review_pending
- event_users[Results::IDV_FINAL_RESOLUTION_GPO_IN_PERSON_FRAUD_REVIEW] << user_id if fraud_review_pending
+ users[Results::IDV_FINAL_RESOLUTION_GPO_IN_PERSON] << user_id if !fraud_review_pending
+ users[Results::IDV_FINAL_RESOLUTION_GPO_IN_PERSON_FRAUD_REVIEW] << user_id if fraud_review_pending
end
when Events::IDV_DOC_AUTH_IMAGE_UPLOAD
- event_users[Results::IDV_REJECT_DOC_AUTH] << user_id if row['doc_auth_failed_non_fraud'] == '1'
+ users[Results::IDV_REJECT_DOC_AUTH] << user_id if row['doc_auth_failed_non_fraud'] == '1'
when Events::IDV_DOC_AUTH_VERIFY_RESULTS
- event_users[Results::IDV_REJECT_VERIFY] << user_id if success == '0'
+ users[Results::IDV_REJECT_VERIFY] << user_id if success == '0'
when Events::IDV_PHONE_FINDER_RESULTS
- event_users[Results::IDV_REJECT_PHONE_FINDER] << user_id if success == '0'
+ users[Results::IDV_REJECT_PHONE_FINDER] << user_id if success == '0'
end
end
- event_users
+ users
end
end
# rubocop:enable Metrics/BlockLength
@@ -337,6 +384,25 @@ def query
),
idv_final_resolution: quote(Events::IDV_FINAL_RESOLUTION),
fraud_review_passed: quote(Events::FRAUD_REVIEW_PASSED),
+ fraud_event_names: quote(
+ [
+ Events::FRAUD_REVIEW_PASSED,
+ Events::FRAUD_REVIEW_REJECT_AUTOMATIC,
+ Events::FRAUD_REVIEW_REJECT_MANUAL,
+ ],
+ ),
+ normalized_fraud_review_pending: "(#{[
+ # rubocop:disable Layout/LineLength
+ 'coalesce(properties.event_properties.fraud_review_pending, 0)',
+ # NOTE: fraud_pending_reason is present on 'IdV: final resolution' events. For GPO / IPP,
+ # it will be set but the fraud_review_pending flag will be set to 0.
+ # To calculate the 'Workflow completed - GPO Pending - Fraud Review' stat, we
+ # must consider this independently of fraud_review_pending.
+ '!isblank(properties.event_properties.fraud_pending_reason)',
+ 'coalesce(properties.event_properties.fraud_check_failed, 0)',
+ 'coalesce((ispresent(properties.event_properties.tmx_status) and properties.event_properties.tmx_status in ["threatmetrix_review", "threatmetrix_reject"]), 0)',
+ # rubocop:enable Layout/LineLength
+ ].join(" OR ")})",
}
format(<<~QUERY, params)
@@ -344,16 +410,17 @@ def query
name
, properties.user_id AS user_id
, coalesce(properties.event_properties.success, 0) AS success
- #{issuers.present? ? '| filter properties.service_provider IN %{issuers}' : ''}
+ , coalesce(properties.service_provider, properties.event_properties.issuer) AS service_provider
| filter name in %{event_names}
- | filter (name = %{usps_enrollment_status_updated} and properties.event_properties.passed = 1 and properties.event_properties.tmx_status not in ['threatmetrix_review', 'threatmetrix_reject'])
+ | filter (name = %{usps_enrollment_status_updated} and properties.event_properties.passed = 1)
or (name != %{usps_enrollment_status_updated})
- | filter (name in %{gpo_verification_submitted} and properties.event_properties.success = 1 and !properties.event_properties.pending_in_person_enrollment and !properties.event_properties.fraud_check_failed)
+ | filter (name in %{gpo_verification_submitted} and properties.event_properties.success = 1 and !properties.event_properties.pending_in_person_enrollment)
or (name not in %{gpo_verification_submitted})
| filter (name = %{fraud_review_passed} and properties.event_properties.success = 1)
or (name != %{fraud_review_passed})
+ #{issuers.present? ? '| filter service_provider IN %{issuers} OR name IN %{fraud_event_names}' : ''}
| fields
- coalesce(properties.event_properties.fraud_review_pending, properties.event_properties.fraud_pending_reason, 0) AS fraud_review_pending
+ %{normalized_fraud_review_pending} AS fraud_review_pending
, coalesce(properties.event_properties.gpo_verification_pending, 0) AS gpo_verification_pending
, coalesce(properties.event_properties.in_person_verification_pending, 0) AS in_person_verification_pending
, ispresent(properties.event_properties.deactivation_reason) AS has_other_deactivation_reason
@@ -373,6 +440,10 @@ def cloudwatch_client
logger: verbose? ? Logger.new(STDERR) : nil,
)
end
+
+ def sp_key(issuer)
+ "sp:#{issuer}"
+ end
end
end
diff --git a/lib/tasks/check_for_pending_migrations.rake b/lib/tasks/check_for_pending_migrations.rake
index c038c71401e..05a4e470900 100644
--- a/lib/tasks/check_for_pending_migrations.rake
+++ b/lib/tasks/check_for_pending_migrations.rake
@@ -12,7 +12,7 @@ namespace :db do
)
warn('Skipping pending migration check, idp_run_migrations=true')
else
- ActiveRecord::Migration.check_pending!(ActiveRecord::Base.connection)
+ ActiveRecord::Migration.check_all_pending!
end
end
end
diff --git a/spec/controllers/concerns/two_factor_authenticatable_methods_spec.rb b/spec/controllers/concerns/two_factor_authenticatable_methods_spec.rb
index f14f9ec9c32..9fe0c767aac 100644
--- a/spec/controllers/concerns/two_factor_authenticatable_methods_spec.rb
+++ b/spec/controllers/concerns/two_factor_authenticatable_methods_spec.rb
@@ -36,6 +36,7 @@
multi_factor_auth_method: TwoFactorAuthenticatable::AuthMethod::REMEMBER_DEVICE,
enabled_mfa_methods_count: 0,
new_device: true,
+ attempts: 1,
)
end
@@ -189,6 +190,7 @@
multi_factor_auth_method: TwoFactorAuthenticatable::AuthMethod::SMS,
enabled_mfa_methods_count: 1,
new_device: true,
+ attempts: 1,
)
end
@@ -197,5 +199,33 @@
expect(user.events.last.event_type).to eq('sign_in_unsuccessful_2fa')
end
end
+
+ context 'user switches mfa after unsuccessful attempt' do
+ let(:user) { create(:user, :fully_registered) }
+ let(:auth_method) { TwoFactorAuthenticatable::AuthMethod::SMS }
+ before do
+ allow(controller).to receive(:user_session).and_return(
+ mfa_attempts: {
+ auth_method: 'piv_cac', attempts: 2
+ },
+ )
+ end
+
+ it 'tracks multi-factor authentication event with the expected number of attempts' do
+ stub_analytics
+
+ result
+
+ expect(@analytics).to have_logged_event(
+ 'Multi-Factor Authentication',
+ success: true,
+ errors: {},
+ multi_factor_auth_method: TwoFactorAuthenticatable::AuthMethod::SMS,
+ enabled_mfa_methods_count: 1,
+ new_device: true,
+ attempts: 1,
+ )
+ end
+ end
end
end
diff --git a/spec/controllers/idv/cancellations_controller_spec.rb b/spec/controllers/idv/cancellations_controller_spec.rb
index fad25f004ac..12ef3b256c1 100644
--- a/spec/controllers/idv/cancellations_controller_spec.rb
+++ b/spec/controllers/idv/cancellations_controller_spec.rb
@@ -227,8 +227,7 @@
before do
allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
allow(controller).to receive(:user_session).and_return(
- 'idv/in_person' => { 'pii_from_user' => {},
- 'Idv::Steps::InPerson::StateIdStep' => true },
+ 'idv/in_person' => { 'pii_from_user' => {} },
)
delete :destroy
enrollment.reload
@@ -251,8 +250,7 @@
before do
allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
allow(controller).to receive(:user_session).and_return(
- 'idv/in_person' => { 'pii_from_user' => {},
- 'Idv::Steps::InPerson::StateIdStep' => true },
+ 'idv/in_person' => { 'pii_from_user' => {} },
)
delete :destroy
diff --git a/spec/controllers/idv/in_person/address_controller_spec.rb b/spec/controllers/idv/in_person/address_controller_spec.rb
index c44a4f745b0..1701eb459d9 100644
--- a/spec/controllers/idv/in_person/address_controller_spec.rb
+++ b/spec/controllers/idv/in_person/address_controller_spec.rb
@@ -33,32 +33,11 @@
end
context '#confirm_in_person_state_id_step_complete' do
- context 'in_person_state_id_controller_enabled is enabled' do
- before do
- allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).
- and_return(true)
- end
-
- it 'redirects to state id page if not complete' do
- subject.user_session['idv/in_person'][:pii_from_user].delete(:identity_doc_address1)
- get :show
-
- expect(response).to redirect_to idv_in_person_proofing_state_id_url
- end
- end
-
- context 'in_person_state_id_controller_enabled is not enabled' do
- before do
- allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).
- and_return(false)
- end
-
- it 'redirects to state id page if not complete' do
- subject.user_session['idv/in_person'][:pii_from_user].delete(:identity_doc_address1)
- get :show
+ it 'redirects to state id page if not complete' do
+ subject.user_session['idv/in_person'][:pii_from_user].delete(:identity_doc_address1)
+ get :show
- expect(response).to redirect_to idv_in_person_step_url(step: :state_id)
- end
+ expect(response).to redirect_to idv_in_person_proofing_state_id_url
end
end
diff --git a/spec/controllers/idv/in_person/public/usps_locations_controller_spec.rb b/spec/controllers/idv/in_person/public/usps_locations_controller_spec.rb
index 6dc427fdf5f..3559670d7b4 100644
--- a/spec/controllers/idv/in_person/public/usps_locations_controller_spec.rb
+++ b/spec/controllers/idv/in_person/public/usps_locations_controller_spec.rb
@@ -17,29 +17,9 @@
}
end
- context 'with feature flag on' do
- before do
- allow(IdentityConfig.store).to receive(:in_person_public_address_search_enabled).
- and_return(true)
- end
-
- it 'is successful and has a response' do
- action
- expect(response).to be_ok
- end
- end
-
- context 'with feature flag off' do
- before do
- allow(IdentityConfig.store).to receive(:in_person_public_address_search_enabled).
- and_return(false)
- end
-
- it 'is a 400' do
- action
-
- expect(response).to be_not_found
- end
+ it 'is successful and has a response' do
+ action
+ expect(response).to be_ok
end
end
end
diff --git a/spec/controllers/idv/in_person/state_id_controller_spec.rb b/spec/controllers/idv/in_person/state_id_controller_spec.rb
index f977cdf27fc..5fa3770ebcb 100644
--- a/spec/controllers/idv/in_person/state_id_controller_spec.rb
+++ b/spec/controllers/idv/in_person/state_id_controller_spec.rb
@@ -8,8 +8,6 @@
let(:enrollment) { InPersonEnrollment.new }
before do
- allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).
- and_return(true)
allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled).
and_return(true)
stub_sign_in(user)
diff --git a/spec/controllers/idv/in_person_controller_spec.rb b/spec/controllers/idv/in_person_controller_spec.rb
index ec3d730ceea..e29158a1562 100644
--- a/spec/controllers/idv/in_person_controller_spec.rb
+++ b/spec/controllers/idv/in_person_controller_spec.rb
@@ -101,7 +101,7 @@
it 'finishes the flow' do
put :update, params: { step: 'state_id' }
- expect(response).to redirect_to idv_in_person_address_url
+ expect(response).to redirect_to idv_in_person_proofing_state_id_path
end
end
end
diff --git a/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb
index 2b474fdf885..14e6da8293f 100644
--- a/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb
@@ -41,6 +41,7 @@
multi_factor_auth_method_created_at: Time.zone.now.strftime('%s%L'),
enabled_mfa_methods_count: 1,
new_device: true,
+ attempts: 1,
)
expect(subject.user_session[:auth_events]).to eq(
@@ -97,6 +98,7 @@
multi_factor_auth_method_created_at: Time.zone.now.strftime('%s%L'),
enabled_mfa_methods_count: 1,
new_device: true,
+ attempts: 1,
)
expect(@analytics).to have_logged_event(
'User marked authenticated',
@@ -175,6 +177,7 @@
multi_factor_auth_method: 'backup_code',
enabled_mfa_methods_count: 1,
new_device: true,
+ attempts: 1,
)
expect(@analytics).to have_logged_event('Multi-Factor Authentication: max attempts reached')
end
diff --git a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb
index cf02c712fd8..9fabd936c50 100644
--- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb
@@ -152,6 +152,7 @@
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
enabled_mfa_methods_count: 1,
in_account_creation_flow: false,
+ attempts: 1,
)
end
@@ -234,6 +235,7 @@
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
enabled_mfa_methods_count: 1,
in_account_creation_flow: false,
+ attempts: 1,
)
expect(@analytics).to have_logged_event('Multi-Factor Authentication: max attempts reached')
end
@@ -304,6 +306,7 @@
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
enabled_mfa_methods_count: 1,
in_account_creation_flow: false,
+ attempts: 1,
)
expect(@analytics).to have_logged_event(
'User marked authenticated',
@@ -351,6 +354,7 @@
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
enabled_mfa_methods_count: 1,
in_account_creation_flow: false,
+ attempts: 1,
)
expect(@analytics).to have_logged_event(
'User marked authenticated',
@@ -535,6 +539,7 @@
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
enabled_mfa_methods_count: 1,
in_account_creation_flow: true,
+ attempts: 1,
)
end
@@ -604,12 +609,20 @@
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
enabled_mfa_methods_count: 1,
in_account_creation_flow: false,
+ attempts: 1,
)
end
context 'user enters in valid code after invalid entry' do
before do
expect(subject.current_user.reload.second_factor_attempts_count).to eq 1
+ post(
+ :create,
+ params: {
+ code: '999',
+ otp_delivery_preference: 'sms',
+ },
+ )
post(
:create,
params: {
@@ -621,6 +634,26 @@
it 'resets second_factor_attempts_count' do
expect(subject.current_user.reload.second_factor_attempts_count).to eq 0
end
+
+ it 'tracks an event' do
+ expect(@analytics).to have_logged_event(
+ 'Multi-Factor Authentication Setup',
+ success: false,
+ error_details: { code: { wrong_length: true, incorrect: true } },
+ confirmation_for_add_phone: true,
+ context: 'confirmation',
+ multi_factor_auth_method: 'sms',
+ phone_configuration_id: controller.current_user.default_phone_configuration.id,
+ multi_factor_auth_method_created_at: controller.current_user.
+ default_phone_configuration.created_at.strftime('%s%L'),
+ area_code: parsed_phone.area_code,
+ country_code: parsed_phone.country,
+ phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
+ enabled_mfa_methods_count: 1,
+ in_account_creation_flow: false,
+ attempts: 1,
+ )
+ end
end
end
@@ -669,6 +702,7 @@
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
enabled_mfa_methods_count: 0,
in_account_creation_flow: false,
+ attempts: 1,
)
expect(controller).to have_received(:create_user_event).with(:phone_confirmed)
diff --git a/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb
index d376cc825b9..09af3ac9392 100644
--- a/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb
@@ -76,6 +76,7 @@
multi_factor_auth_method: 'personal-key',
multi_factor_auth_method_created_at:,
new_device: true,
+ attempts: 1,
)
expect(@analytics).to have_logged_event(
'Personal key: Alert user about sign in',
@@ -218,6 +219,7 @@
multi_factor_auth_method: 'personal-key',
multi_factor_auth_method_created_at: personal_key_generated_at.strftime('%s%L'),
new_device: true,
+ attempts: 1,
)
expect(@analytics).to have_logged_event('Multi-Factor Authentication: max attempts reached')
end
diff --git a/spec/controllers/two_factor_authentication/piv_cac_mismatch_controller_spec.rb b/spec/controllers/two_factor_authentication/piv_cac_mismatch_controller_spec.rb
new file mode 100644
index 00000000000..fdf1d2caef8
--- /dev/null
+++ b/spec/controllers/two_factor_authentication/piv_cac_mismatch_controller_spec.rb
@@ -0,0 +1,178 @@
+require 'rails_helper'
+
+RSpec.describe TwoFactorAuthentication::PivCacMismatchController do
+ let(:user) { create(:user, :with_piv_or_cac) }
+
+ before do
+ stub_sign_in_before_2fa(user) if user
+ end
+
+ describe '#show' do
+ subject(:response) { get :show }
+
+ context 'with user having piv as their only authentication method' do
+ let(:user) { create(:user, :with_piv_or_cac) }
+
+ it 'assigns has_other_authentication_methods as false' do
+ response
+
+ expect(assigns(:has_other_authentication_methods)).to eq(false)
+ end
+
+ it 'logs an analytics event' do
+ stub_analytics
+
+ response
+
+ expect(@analytics).to have_logged_event(
+ :piv_cac_mismatch_visited,
+ piv_cac_required: false,
+ has_other_authentication_methods: false,
+ )
+ end
+ end
+
+ context 'with user having other authentication methods' do
+ let(:user) { create(:user, :with_piv_or_cac, :with_phone) }
+
+ it 'assigns has_other_authentication_methods as true' do
+ response
+
+ expect(assigns(:has_other_authentication_methods)).to eq(true)
+ end
+
+ it 'logs an analytics event' do
+ stub_analytics
+
+ response
+
+ expect(@analytics).to have_logged_event(
+ :piv_cac_mismatch_visited,
+ piv_cac_required: false,
+ has_other_authentication_methods: true,
+ )
+ end
+ end
+
+ context 'with partner not requiring hspd12 authentication' do
+ before do
+ controller.session[:sp] = {
+ issuer: SamlAuthHelper::SP_ISSUER,
+ acr_values: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF,
+ }
+ end
+
+ it 'assigns piv_cac_required as false' do
+ response
+
+ expect(assigns(:piv_cac_required)).to eq(false)
+ end
+
+ it 'logs an analytics event' do
+ stub_analytics
+
+ response
+
+ expect(@analytics).to have_logged_event(
+ :piv_cac_mismatch_visited,
+ piv_cac_required: false,
+ has_other_authentication_methods: false,
+ )
+ end
+ end
+
+ context 'with partner requiring hspd12 authentication' do
+ before do
+ controller.session[:sp] = {
+ issuer: SamlAuthHelper::SP_ISSUER,
+ acr_values: Saml::Idp::Constants::AAL2_HSPD12_AUTHN_CONTEXT_CLASSREF,
+ }
+ end
+
+ it 'assigns piv_cac_required as true' do
+ response
+
+ expect(assigns(:piv_cac_required)).to eq(true)
+ end
+
+ it 'logs an analytics event' do
+ stub_analytics
+
+ response
+
+ expect(@analytics).to have_logged_event(
+ :piv_cac_mismatch_visited,
+ piv_cac_required: true,
+ has_other_authentication_methods: false,
+ )
+ end
+ end
+
+ context 'if user is not signed in' do
+ let(:user) { nil }
+
+ it 'redirects user to sign in' do
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'if user is already authenticated' do
+ let(:user) { nil }
+
+ before do
+ stub_sign_in
+ end
+
+ it 'redirects user to the signed in path' do
+ expect(response).to redirect_to(account_path)
+ end
+ end
+ end
+
+ describe '#create' do
+ let(:params) { {} }
+ subject(:response) { post :create, params: params }
+
+ context 'when user chooses to add piv' do
+ let(:params) { { add_piv_cac_after_2fa: 'true' } }
+
+ it 'assigns session value to add piv after authenticating' do
+ response
+
+ expect(controller.user_session[:add_piv_cac_after_2fa]).to eq(true)
+ end
+
+ it 'logs an analytics event' do
+ stub_analytics
+
+ response
+
+ expect(@analytics).to have_logged_event(
+ :piv_cac_mismatch_submitted,
+ add_piv_cac_after_2fa: true,
+ )
+ end
+ end
+
+ context 'when user chooses to skip adding piv' do
+ let(:params) { {} }
+
+ it 'assigns session value to skip adding piv after authenticating' do
+ response
+
+ expect(controller.user_session[:add_piv_cac_after_2fa]).to eq(false)
+ end
+
+ it 'logs an analytics event' do
+ stub_analytics
+
+ response
+
+ expect(@analytics).to have_logged_event(
+ :piv_cac_mismatch_submitted,
+ add_piv_cac_after_2fa: false,
+ )
+ end
+ end
+ end
+end
diff --git a/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb
index d2c04d1ce67..70dab21a99a 100644
--- a/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb
@@ -34,11 +34,7 @@
'key_id' => 'foo',
)
allow(PivCacService).to receive(:decode_token).with('bad-token').and_return(
- 'uuid' => 'bad-uuid',
- 'subject' => bad_dn,
- 'issuer' => x509_issuer,
- 'nonce' => nonce,
- 'key_id' => 'foo',
+ 'error' => 'token.bad',
)
allow(PivCacService).to receive(:decode_token).with('bad-nonce').and_return(
'uuid' => user.piv_cac_configurations.first.x509_dn_uuid,
@@ -135,6 +131,7 @@
piv_cac_configuration_id: cfg.id,
piv_cac_configuration_dn_uuid: cfg.x509_dn_uuid,
key_id: 'foo',
+ attempts: 1,
)
expect(@analytics).to have_logged_event(
'User marked authenticated',
@@ -205,26 +202,50 @@
end
context 'when the user presents a different piv/cac' do
+ subject(:response) { get :show, params: { token: 'good-other-token' } }
+
before do
stub_sign_in_before_2fa(user)
-
- get :show, params: { token: 'good-other-token' }
end
it 'increments second_factor_attempts_count' do
- expect(subject.current_user.reload.second_factor_attempts_count).to eq 1
- end
+ response
- it 'redirects to the piv/cac entry screen' do
- expect(response).to redirect_to login_two_factor_piv_cac_path
+ expect(controller.current_user.reload.second_factor_attempts_count).to eq 1
end
- it 'displays flash error message' do
- expect(flash[:error]).to eq t('two_factor_authentication.invalid_piv_cac')
+ it 'redirects to the piv/cac mismatch screen' do
+ expect(response).to redirect_to login_two_factor_piv_cac_mismatch_path
end
it 'resets the piv/cac session information' do
- expect(subject.user_session[:decrypted_x509]).to be_nil
+ response
+
+ expect(controller.user_session[:decrypted_x509]).to be_nil
+ end
+
+ context 'when user session context is not authentication' do
+ before do
+ allow(UserSessionContext).to receive(:authentication_context?).and_return(false)
+ end
+
+ it 'redirects to authenticate again, including error message' do
+ expect(response).to redirect_to login_two_factor_piv_cac_path
+ expect(flash[:error]).to eq t('two_factor_authentication.invalid_piv_cac')
+ end
+ end
+
+ context 'when user has maximum number of piv/cac associated with their account' do
+ before do
+ while user.piv_cac_configurations.count < IdentityConfig.store.max_piv_cac_per_account
+ create(:piv_cac_configuration, user:)
+ end
+ end
+
+ it 'redirects to authenticate again, including error message' do
+ expect(response).to redirect_to login_two_factor_piv_cac_path
+ expect(flash[:error]).to eq t('two_factor_authentication.invalid_piv_cac')
+ end
end
end
@@ -239,8 +260,6 @@
stub_analytics
- piv_cac_mismatch = { type: 'user.piv_cac_mismatch' }
-
expect(PushNotification::HttpPush).to receive(:deliver).
with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user))
@@ -250,13 +269,12 @@
expect(@analytics).to have_logged_event(
'Multi-Factor Authentication',
success: false,
- errors: piv_cac_mismatch,
+ errors: { type: 'token.invalid' },
context: 'authentication',
multi_factor_auth_method: 'piv_cac',
enabled_mfa_methods_count: 2,
new_device: true,
- key_id: 'foo',
- piv_cac_configuration_dn_uuid: 'bad-uuid',
+ attempts: 1,
)
expect(@analytics).to have_logged_event('Multi-Factor Authentication: max attempts reached')
end
diff --git a/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb
index 8d273b1df37..56884e53e67 100644
--- a/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb
@@ -59,6 +59,7 @@
multi_factor_auth_method_created_at: cfg.created_at.strftime('%s%L'),
new_device: true,
auth_app_configuration_id: controller.current_user.auth_app_configurations.first.id,
+ attempts: 1,
)
expect(@analytics).to have_logged_event(
'User marked authenticated',
@@ -177,6 +178,7 @@
enabled_mfa_methods_count: 2,
multi_factor_auth_method: 'totp',
new_device: true,
+ attempts: 1,
)
expect(@analytics).to have_logged_event('Multi-Factor Authentication: max attempts reached')
end
diff --git a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb
index 0c8732018ef..6503d7a66e8 100644
--- a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb
@@ -158,6 +158,7 @@
webauthn_configuration_id: webauthn_configuration.id,
multi_factor_auth_method_created_at: webauthn_configuration.created_at.strftime('%s%L'),
new_device: true,
+ attempts: 1,
)
expect(@analytics).to have_logged_event(
'User marked authenticated',
@@ -218,6 +219,7 @@
multi_factor_auth_method_created_at: webauthn_configuration.created_at.
strftime('%s%L'),
new_device: true,
+ attempts: 1,
)
expect(@analytics).to have_logged_event(
'User marked authenticated',
@@ -249,6 +251,7 @@
webauthn_configuration_id: webauthn_configuration.id,
multi_factor_auth_method_created_at: webauthn_configuration.created_at.strftime('%s%L'),
new_device: true,
+ attempts: 1,
)
end
@@ -315,6 +318,7 @@
second_webauthn_platform_configuration.created_at.strftime('%s%L'),
new_device: true,
frontend_error: webauthn_error,
+ attempts: 1,
)
end
end
diff --git a/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb b/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb
index 3656a2d72ac..1d1e42bcef7 100644
--- a/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb
+++ b/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb
@@ -13,18 +13,11 @@
end
describe '#new' do
- context 'when not signed in' do
- it 'redirects to root url' do
- get :new
-
- expect(response).to redirect_to(root_url)
- end
- end
+ let(:params) { nil }
+ subject(:response) { get :new, params: params }
context 'when signed out' do
it 'redirects to sign in page' do
- get :new
-
expect(response).to redirect_to(new_user_session_url)
end
end
@@ -37,15 +30,37 @@
end
it 'redirects to 2fa entry' do
- get :new
-
expect(response).to redirect_to(user_two_factor_authentication_url)
end
end
context 'when signed in' do
+ let(:user) { create(:user, :fully_registered) }
before { stub_sign_in(user) }
+ it 'assigns piv_cac_required instance variable as false' do
+ response
+
+ expect(assigns(:piv_cac_required)).to eq(false)
+ end
+
+ context 'when SP requires PIV/CAC' do
+ let(:service_provider) { create(:service_provider) }
+
+ before do
+ controller.session[:sp] = {
+ issuer: service_provider.issuer,
+ acr_values: Saml::Idp::Constants::AAL2_HSPD12_AUTHN_CONTEXT_CLASSREF,
+ }
+ end
+
+ it 'assigns piv_cac_required instance variable as true' do
+ response
+
+ expect(assigns(:piv_cac_required)).to eq(true)
+ end
+ end
+
context 'without associated piv/cac' do
let(:user) do
create(:user, :fully_registered, with: { phone: '+1 (703) 555-0000' })
@@ -55,8 +70,8 @@
before(:each) do
allow(PivCacService).to receive(:decode_token).with(good_token) { good_token_response }
allow(PivCacService).to receive(:decode_token).with(bad_token) { bad_token_response }
- allow(subject).to receive(:user_session).and_return(piv_cac_nonce: nonce)
- subject.user_session[:piv_cac_nickname] = nickname
+ allow(controller).to receive(:user_session).and_return(piv_cac_nonce: nonce)
+ controller.user_session[:piv_cac_nickname] = nickname
end
let(:nonce) { 'nonce' }
@@ -80,14 +95,13 @@
context 'when rendered without a token' do
it 'renders the "new" template' do
- get :new
expect(response).to render_template(:new)
end
it 'tracks the analytic event of visited' do
stub_analytics
- get :new
+ response
expect(@analytics).to have_logged_event(
:piv_cac_setup_visited,
@@ -98,75 +112,143 @@
end
context 'when redirected with a good token' do
- let(:user) do
- create(:user)
- end
+ let(:params) { { token: good_token } }
+ let(:user) { create(:user) }
let(:mfa_selections) { ['piv_cac', 'voice'] }
+
before do
- subject.user_session[:mfa_selections] = mfa_selections
+ controller.user_session[:mfa_selections] = mfa_selections
end
context 'with no additional MFAs chosen on setup' do
let(:mfa_selections) { ['piv_cac'] }
it 'redirects to suggest 2nd MFA page' do
- get :new, params: { token: good_token }
+ stub_analytics
+
expect(response).to redirect_to(auth_method_confirmation_url)
+
+ expect(@analytics).to have_logged_event(
+ 'Multi-Factor Authentication Setup',
+ enabled_mfa_methods_count: 1,
+ errors: {},
+ multi_factor_auth_method: 'piv_cac',
+ in_account_creation_flow: false,
+ success: true,
+ attempts: 1,
+ )
+ end
+
+ it 'logs mfa attempts commensurate to number of attempts' do
+ stub_analytics
+
+ get :new, params: { token: bad_token }
+ response
+
+ expect(@analytics).to have_logged_event(
+ 'Multi-Factor Authentication Setup',
+ enabled_mfa_methods_count: 1,
+ errors: {},
+ multi_factor_auth_method: 'piv_cac',
+ in_account_creation_flow: false,
+ success: true,
+ attempts: 2,
+ )
end
it 'sets the piv/cac session information' do
- get :new, params: { token: good_token }
+ response
+
json = {
'subject' => 'some dn',
'issuer' => nil,
'presented' => true,
}.to_json
- expect(subject.user_session[:decrypted_x509]).to eq json
+ expect(controller.user_session[:decrypted_x509]).to eq json
end
it 'sets the session to not require piv setup upon sign-in' do
- get :new, params: { token: good_token }
+ response
+
+ expect(controller.session[:needs_to_setup_piv_cac_after_sign_in]).to eq false
+ end
+
+ context 'when user adds after piv cac mismatch error' do
+ before do
+ controller.user_session[:add_piv_cac_after_2fa] = true
+ end
+
+ it 'deletes add_piv_cac_after_2fa session value' do
+ response
- expect(subject.session[:needs_to_setup_piv_cac_after_sign_in]).to eq false
+ expect(controller.user_session).not_to have_key(:add_piv_cac_after_2fa)
+ end
end
end
context 'with additional MFAs leftover' do
it 'redirects to Mfa Confirmation page' do
- get :new, params: { token: good_token }
expect(response).to redirect_to(phone_setup_url)
end
it 'sets the piv/cac session information' do
- get :new, params: { token: good_token }
+ response
+
json = {
'subject' => 'some dn',
'issuer' => nil,
'presented' => true,
}.to_json
- expect(subject.user_session[:decrypted_x509]).to eq json
+ expect(controller.user_session[:decrypted_x509]).to eq json
end
it 'sets the session to not require piv setup upon sign-in' do
- get :new, params: { token: good_token }
+ response
- expect(subject.session[:needs_to_setup_piv_cac_after_sign_in]).to eq false
+ expect(controller.session[:needs_to_setup_piv_cac_after_sign_in]).to eq false
end
end
end
context 'when redirected with an error token' do
+ let(:params) { { token: bad_token } }
+
it 'renders the error template' do
- get :new, params: { token: bad_token }
expect(response).to redirect_to setup_piv_cac_error_path(error: 'certificate.bad')
end
it 'resets the piv/cac session information' do
- expect(subject.user_session[:decrypted_x509]).to be_nil
+ response
+
+ expect(controller.user_session[:decrypted_x509]).to be_nil
end
end
end
end
end
+
+ describe '#submit_new_piv_cac' do
+ let(:user) { create(:user, :fully_registered) }
+
+ before { stub_sign_in(user) }
+
+ context 'when user opts to skip adding piv cac after 2fa' do
+ subject(:response) { post :submit_new_piv_cac, params: { skip: 'true' } }
+
+ before do
+ allow(controller).to receive(:user_session).and_return(add_piv_cac_after_2fa: true)
+ end
+
+ it 'deletes add_piv_cac_after_2fa session value' do
+ response
+
+ expect(controller.user_session).not_to have_key(:add_piv_cac_after_2fa)
+ end
+
+ it 'redirects to after sign in path' do
+ expect(response).to redirect_to(account_path)
+ end
+ end
+ end
end
diff --git a/spec/controllers/users/totp_setup_controller_spec.rb b/spec/controllers/users/totp_setup_controller_spec.rb
index 0d865fc2e1f..cb4bd345ae5 100644
--- a/spec/controllers/users/totp_setup_controller_spec.rb
+++ b/spec/controllers/users/totp_setup_controller_spec.rb
@@ -108,6 +108,7 @@
multi_factor_auth_method: 'totp',
enabled_mfa_methods_count: 0,
in_account_creation_flow: false,
+ attempts: 1,
)
end
end
@@ -136,6 +137,38 @@
auth_app_configuration_id: next_auth_app_id,
enabled_mfa_methods_count: 2,
in_account_creation_flow: false,
+ attempts: 1,
+ )
+ end
+ end
+
+ context 'when user presents correct code after submitting an incorrect code' do
+ before do
+ user = create(:user, :fully_registered)
+ secret = ROTP::Base32.random_base32
+ stub_sign_in(user)
+ stub_analytics
+
+ subject.user_session[:new_totp_secret] = 'abcdehij'
+
+ patch :confirm, params: { name: name, code: 123 }
+
+ subject.user_session[:new_totp_secret] = secret
+
+ patch :confirm, params: { name: name, code: generate_totp_code(secret) }
+ end
+
+ it 'logs correct events' do
+ expect(@analytics).to have_logged_event(
+ 'Multi-Factor Authentication Setup',
+ success: true,
+ errors: {},
+ totp_secret_present: true,
+ multi_factor_auth_method: 'totp',
+ auth_app_configuration_id: next_auth_app_id,
+ enabled_mfa_methods_count: 2,
+ in_account_creation_flow: false,
+ attempts: 2,
)
end
end
@@ -164,6 +197,7 @@
multi_factor_auth_method: 'totp',
enabled_mfa_methods_count: 1,
in_account_creation_flow: false,
+ attempts: 1,
)
end
end
@@ -193,6 +227,7 @@
multi_factor_auth_method: 'totp',
enabled_mfa_methods_count: 1,
in_account_creation_flow: false,
+ attempts: 1,
)
end
end
@@ -221,6 +256,7 @@
multi_factor_auth_method: 'totp',
enabled_mfa_methods_count: 0,
in_account_creation_flow: false,
+ attempts: 1,
)
end
end
@@ -252,6 +288,7 @@
auth_app_configuration_id: next_auth_app_id,
enabled_mfa_methods_count: 1,
in_account_creation_flow: true,
+ attempts: 1,
)
end
end
@@ -271,6 +308,7 @@
auth_app_configuration_id: next_auth_app_id,
enabled_mfa_methods_count: 1,
in_account_creation_flow: true,
+ attempts: 1,
)
end
end
@@ -297,6 +335,7 @@
multi_factor_auth_method: 'totp',
enabled_mfa_methods_count: 0,
in_account_creation_flow: false,
+ attempts: 1,
)
end
end
diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb
index b801a775bdc..3354d15bfeb 100644
--- a/spec/controllers/users/webauthn_setup_controller_spec.rb
+++ b/spec/controllers/users/webauthn_setup_controller_spec.rb
@@ -121,6 +121,7 @@
at: true,
ed: false,
},
+ attempts: 1,
)
expect(@analytics).to have_logged_event(
:webauthn_setup_submitted,
@@ -249,6 +250,7 @@
mfa_method_counts: { webauthn: 1 },
multi_factor_auth_method: 'webauthn',
success: true,
+ attempts: 1,
)
expect(@analytics).to have_logged_event(
:webauthn_setup_submitted,
@@ -308,6 +310,7 @@
mfa_method_counts: { webauthn_platform: 1 },
multi_factor_auth_method: 'webauthn_platform',
success: true,
+ attempts: 1,
)
end
@@ -354,6 +357,7 @@
mfa_method_counts: {},
multi_factor_auth_method: 'webauthn_platform',
success: false,
+ attempts: 1,
},
)
end
@@ -369,5 +373,57 @@
end
end
end
+
+ context 'sign in and confirm' do
+ let(:params) do
+ {
+ attestation_object: attestation_object,
+ client_data_json: setup_client_data_json,
+ name: 'mykey',
+ transports: 'usb',
+ authenticator_data_value: '65',
+ }
+ end
+
+ before do
+ controller.user_session[:in_account_creation_flow] = true
+ allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:3000')
+ request.host = 'localhost:3000'
+ controller.user_session[:webauthn_challenge] = webauthn_challenge
+ controller.user_session[:mfa_attempts] = { auth_method: 'webauthn', attempts: 1 }
+ end
+
+ it 'tracks the submission' do
+ Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics)
+
+ patch :confirm, params: params
+
+ expect(@analytics).to have_logged_event(
+ 'Multi-Factor Authentication Setup',
+ enabled_mfa_methods_count: 1,
+ mfa_method_counts: {
+ webauthn: 1,
+ },
+ multi_factor_auth_method: 'webauthn',
+ success: true,
+ errors: {},
+ in_account_creation_flow: true,
+ authenticator_data_flags: {
+ up: true,
+ uv: false,
+ be: false,
+ bs: false,
+ at: true,
+ ed: false,
+ },
+ attempts: 2,
+ )
+ expect(@analytics).to have_logged_event(
+ :webauthn_setup_submitted,
+ platform_authenticator: false,
+ success: true,
+ )
+ end
+ end
end
end
diff --git a/spec/factories/piv_cac_configurations.rb b/spec/factories/piv_cac_configurations.rb
index 68c73f0b22b..2a7cea7ddb0 100644
--- a/spec/factories/piv_cac_configurations.rb
+++ b/spec/factories/piv_cac_configurations.rb
@@ -3,7 +3,7 @@
factory :piv_cac_configuration do
name { Faker::Lorem.unique.words.join(' ') }
- x509_dn_uuid { 'helloworld' }
+ x509_dn_uuid { Random.uuid }
user
end
end
diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb
index 463ca6e67aa..f826ac791f3 100644
--- a/spec/features/idv/in_person_spec.rb
+++ b/spec/features/idv/in_person_spec.rb
@@ -9,7 +9,6 @@
before do
allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
- allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).and_return(true)
end
it 'works for a happy path', allow_browser_log: true do
@@ -62,8 +61,10 @@
expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED)
# click update state ID button
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
+
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
+
choose t('in_person_proofing.form.state_id.same_address_as_id_yes')
click_button t('forms.buttons.submit.update')
expect(page).to have_content(t('headings.verify'))
@@ -353,7 +354,7 @@
expect(page).to have_text('new address different from state address1').once
# click update state id address
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# check that the "No, I live at a different address" is checked
expect(page).to have_checked_field(
@@ -368,8 +369,6 @@
before do
allow(IdentityConfig.store).to receive(:in_person_outage_message_enabled).and_return(true)
- allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).
- and_return(true)
end
it 'allows the user to generate a barcode despite outage', allow_browser_log: true do
diff --git a/spec/features/idv/steps/in_person/state_id_50_50_spec.rb b/spec/features/idv/steps/in_person/state_id_50_50_spec.rb
deleted file mode 100644
index 9f155e1ae66..00000000000
--- a/spec/features/idv/steps/in_person/state_id_50_50_spec.rb
+++ /dev/null
@@ -1,381 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe 'state id 50/50 state', :js, allow_browser_log: true do
- include IdvStepHelper
- include InPersonHelper
-
- let(:ipp_service_provider) { create(:service_provider, :active, :in_person_proofing_enabled) }
- let(:user) { user_with_2fa }
-
- before do
- allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
- allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled).and_return(true)
- allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers).
- and_return([ipp_service_provider.issuer])
- allow(IdentityConfig.store).to receive(
- :allowed_valid_authn_contexts_semantic_providers,
- ).and_return([ipp_service_provider.issuer])
- end
-
- context 'when navigating to state id page from PO search location page' do
- context 'when the controller is switched from enabled to disabled' do
- before do
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(true)
- visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer })
- sign_in_via_branded_page(user)
- begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in
- complete_prepare_step(user)
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(false)
- complete_location_step
- end
-
- it 'navigates to the FSM state_id route' do
- expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10)
- end
- end
-
- context 'when the controller is switched from disabled to enabled' do
- before do
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(false)
- visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer })
- sign_in_via_branded_page(user)
- begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in
- complete_prepare_step(user)
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(true)
- complete_location_step
- end
-
- it 'navigates to the controller state_id route' do
- expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10)
- end
- end
- end
-
- context 'when refreshing the state id page' do
- context 'when the controller is switched from enabled to disabled' do
- before do
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(true)
- visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer })
- sign_in_via_branded_page(user)
- begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in
- complete_prepare_step(user)
- complete_location_step
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(false)
- page.refresh
- end
-
- it 'renders the state ID controller page without error' do
- expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10)
- end
- end
-
- context 'when the controller is switched from disabled to enabled' do
- before do
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(false)
- visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer })
- sign_in_via_branded_page(user)
- begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in
- complete_prepare_step(user)
- complete_location_step
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(true)
- page.refresh
- end
-
- it 'renders the FSM state_id page' do
- expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10)
- end
- end
- end
-
- context 'when navigating to state id page from verify info page' do
- context 'when the controller is switched from enabled to disabled' do
- before do
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(true)
- visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer })
- sign_in_via_branded_page(user)
- begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in
- complete_prepare_step(user)
- complete_location_step
- complete_state_id_controller(user, same_address_as_id: true)
- complete_ssn_step(user)
- expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info'))
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(false)
- click_button t('idv.buttons.change_state_id_label')
- end
-
- it 'navigates to the FSM state_id route' do
- expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10)
- # state id page has fields that are pre-populated
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.first_name'),
- with: InPersonHelper::GOOD_FIRST_NAME,
- )
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.last_name'),
- with: InPersonHelper::GOOD_LAST_NAME,
- )
- expect(page).to have_field(t('components.memorable_date.month'), with: '10')
- expect(page).to have_field(t('components.memorable_date.day'), with: '6')
- expect(page).to have_field(t('components.memorable_date.year'), with: '1938')
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.state_id_jurisdiction'),
- with: Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction],
- )
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.state_id_number'),
- with: InPersonHelper::GOOD_STATE_ID_NUMBER,
- )
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.address1'),
- with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1,
- )
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.address2'),
- with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2,
- )
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.city'),
- with: InPersonHelper::GOOD_IDENTITY_DOC_CITY,
- )
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.zipcode'),
- with: InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE,
- )
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.identity_doc_address_state'),
- with: Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction],
- )
- expect(page).to have_checked_field(
- t('in_person_proofing.form.state_id.same_address_as_id_yes'),
- visible: false,
- )
- end
- end
-
- context 'when the controller is switched from disabled to enabled' do
- before do
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(false)
- visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer })
- sign_in_via_branded_page(user)
- begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in
- complete_prepare_step(user)
- complete_location_step
- complete_state_id_step(user, same_address_as_id: true)
- complete_ssn_step(user)
- expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info'))
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(true)
- click_button t('idv.buttons.change_state_id_label')
- end
-
- it 'navigates to the controller state_id route' do
- expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10)
- # state id page has fields that are pre-populated
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.first_name'),
- with: InPersonHelper::GOOD_FIRST_NAME,
- )
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.last_name'),
- with: InPersonHelper::GOOD_LAST_NAME,
- )
- expect(page).to have_field(t('components.memorable_date.month'), with: '10')
- expect(page).to have_field(t('components.memorable_date.day'), with: '6')
- expect(page).to have_field(t('components.memorable_date.year'), with: '1938')
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.state_id_jurisdiction'),
- with: Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction],
- )
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.state_id_number'),
- with: InPersonHelper::GOOD_STATE_ID_NUMBER,
- )
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.address1'),
- with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1,
- )
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.address2'),
- with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2,
- )
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.city'),
- with: InPersonHelper::GOOD_IDENTITY_DOC_CITY,
- )
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.zipcode'),
- with: InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE,
- )
- expect(page).to have_field(
- t('in_person_proofing.form.state_id.identity_doc_address_state'),
- with: Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction],
- )
- expect(page).to have_checked_field(
- t('in_person_proofing.form.state_id.same_address_as_id_yes'),
- visible: false,
- )
- end
- end
- end
-
- context 'when updating state id info from the verify info page' do
- let(:first_name_update) { 'Natalya' }
-
- context 'when the controller is switched from enabled to disabled' do
- before do
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(true)
- visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer })
- sign_in_via_branded_page(user)
- begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in
- complete_prepare_step(user)
- complete_location_step
- complete_state_id_controller(user, same_address_as_id: true)
- complete_ssn_step(user)
- expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info'))
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(false)
- click_button t('idv.buttons.change_state_id_label')
- fill_in t('in_person_proofing.form.state_id.first_name'), with: first_name_update
- click_button t('forms.buttons.submit.update')
- end
-
- it 'navigates back to the verify_info page' do
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text(first_name_update)
- end
- end
-
- context 'when the controller is switched from disabled to enabled' do
- before do
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(false)
- visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer })
- sign_in_via_branded_page(user)
- begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in
- complete_prepare_step(user)
- complete_location_step
- complete_state_id_step(user, same_address_as_id: true)
- complete_ssn_step(user)
- expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info'))
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(true)
- click_button t('idv.buttons.change_state_id_label')
- fill_in t('in_person_proofing.form.state_id.first_name'), with: first_name_update
- click_button t('forms.buttons.submit.update')
- end
-
- it 'navigates back to the verify_info page' do
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text(first_name_update)
- end
- end
- end
-
- context 'when navigating to state id page from hybrid PO search location page' do
- let(:phone_number) { '415-555-0199' }
-
- before do
- allow(FeatureManagement).to receive(:doc_capture_polling_enabled?).and_return(true)
- allow(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config|
- @sms_link = config[:link]
- impl.call(**config)
- end.at_least(1).times
- end
-
- context 'when the controller is switched from enabled to disabled' do
- before do
- perform_in_browser(:desktop) do
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(true)
- visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer })
- sign_in_via_branded_page(user)
- complete_doc_auth_steps_before_hybrid_handoff_step
- click_on t('forms.buttons.continue_remote')
- clear_and_fill_in(:doc_auth_phone, phone_number)
- click_send_link
- end
-
- perform_in_browser(:mobile) do
- visit @sms_link
- mock_doc_auth_fail_face_match_fail
- attach_and_submit_images
- click_button t('in_person_proofing.body.cta.button')
- click_idv_continue
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(false)
- complete_location_step
- end
- end
-
- it 'navigates to the FSM state_id route' do
- perform_in_browser(:desktop) do
- expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10)
- end
- end
- end
-
- context 'when the controller is switched from disabled to enabled' do
- before do
- perform_in_browser(:desktop) do
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(false)
- visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer })
- sign_in_via_branded_page(user)
- complete_doc_auth_steps_before_hybrid_handoff_step
- click_on t('forms.buttons.continue_remote')
- clear_and_fill_in(:doc_auth_phone, phone_number)
- click_send_link
- end
-
- perform_in_browser(:mobile) do
- visit @sms_link
- mock_doc_auth_fail_face_match_fail
- attach_and_submit_images
- click_button t('in_person_proofing.body.cta.button')
- click_idv_continue
- allow(IdentityConfig.store).to receive(
- :in_person_state_id_controller_enabled,
- ).and_return(true)
- complete_location_step
- end
- end
-
- it 'navigates to the controller state_id route' do
- perform_in_browser(:desktop) do
- expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10)
- end
- end
- end
- end
-end
diff --git a/spec/features/idv/steps/in_person/state_id_controller_spec.rb b/spec/features/idv/steps/in_person/state_id_controller_spec.rb
index 52275c4d36a..c889cbebdc6 100644
--- a/spec/features/idv/steps/in_person/state_id_controller_spec.rb
+++ b/spec/features/idv/steps/in_person/state_id_controller_spec.rb
@@ -6,7 +6,6 @@
before do
allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
- allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).and_return(true)
end
context 'when visiting state id for the first time' do
@@ -73,7 +72,7 @@
expect(page).to have_current_path(idv_in_person_ssn_url, wait: 10)
complete_ssn_step
expect(page).to have_current_path(idv_in_person_verify_info_url, wait: 10)
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# state id page has fields that are pre-populated
expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10)
@@ -143,7 +142,7 @@
expect(page).to have_content(t('headings.verify'))
expect(page).to have_current_path(idv_in_person_verify_info_path)
# click update state ID button on the verify page
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
# change address
@@ -157,7 +156,7 @@
# expect to see state ID address update on verify twice
expect(page).to have_text('test update address').twice # for state id addr and addr update
# click update state id address
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
# expect "Yes, I live at a different address" is checked"
@@ -178,7 +177,7 @@
expect(page).to have_content(t('headings.verify'))
expect(page).to have_current_path(idv_in_person_verify_info_path)
# click update state ID button on the verify page
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
# change address
@@ -192,7 +191,7 @@
# expect to see state ID address update on verify
expect(page).to have_text('test update address').once # only state id address update
# click update state id address
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
expect(page).to have_checked_field(
@@ -206,7 +205,7 @@
# skip address step
complete_ssn_step(user)
# click update state ID button on the verify page
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
# change address
@@ -225,7 +224,7 @@
# expect to see state ID address update on verify
expect(page).to have_text('test update address').once # only state id address update
# click update state id address
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
# check that the "No, I live at a different address" is checked"
@@ -247,7 +246,7 @@
expect(page).to have_content(t('headings.verify'))
expect(page).to have_current_path(idv_in_person_verify_info_path)
# click update state ID button on the verify page
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
# change address
@@ -262,7 +261,7 @@
# expect to see state ID address update on verify twice
expect(page).to have_text('test update address').twice # for state id addr and addr update
# click update state ID button on the verify page
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
expect(page).to have_checked_field(
@@ -426,7 +425,7 @@
expect(page).to have_text('PR')
# update state ID
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint'))
diff --git a/spec/features/idv/steps/in_person/state_id_step_spec.rb b/spec/features/idv/steps/in_person/state_id_step_spec.rb
index 1d3e5b96353..0eb2a4ed9ec 100644
--- a/spec/features/idv/steps/in_person/state_id_step_spec.rb
+++ b/spec/features/idv/steps/in_person/state_id_step_spec.rb
@@ -6,7 +6,6 @@
before do
allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
- allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).and_return(false)
end
context 'when visiting state id for the first time' do
@@ -40,7 +39,7 @@
click_link t('links.cancel')
click_on t('idv.cancel.actions.keep_going')
- expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10)
+ expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10)
end
it 'allows user to submit valid inputs on form', allow_browser_log: true do
@@ -72,11 +71,11 @@
click_idv_continue
expect(page).to have_current_path(idv_in_person_ssn_url, wait: 10)
complete_ssn_step
- expect(page).to have_current_path(idv_in_person_verify_info_url, wait: 10)
- click_button t('idv.buttons.change_state_id_label')
+ expect(page).to have_current_path(idv_in_person_verify_info_path, wait: 10)
+ click_link t('idv.buttons.change_state_id_label')
# state id page has fields that are pre-populated
- expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10)
+ expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10)
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
expect(page).to have_field(
t('in_person_proofing.form.state_id.first_name'),
@@ -143,7 +142,7 @@
expect(page).to have_content(t('headings.verify'))
expect(page).to have_current_path(idv_in_person_verify_info_path)
# click update state ID button on the verify page
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
# change address
@@ -157,7 +156,7 @@
# expect to see state ID address update on verify twice
expect(page).to have_text('test update address').twice # for state id addr and addr update
# click update state id address
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
# expect "Yes, I live at a different address" is checked"
@@ -178,7 +177,7 @@
expect(page).to have_content(t('headings.verify'))
expect(page).to have_current_path(idv_in_person_verify_info_path)
# click update state ID button on the verify page
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
# change address
@@ -192,7 +191,7 @@
# expect to see state ID address update on verify
expect(page).to have_text('test update address').once # only state id address update
# click update state id address
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
expect(page).to have_checked_field(
@@ -206,7 +205,7 @@
# skip address step
complete_ssn_step(user)
# click update state ID button on the verify page
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
# change address
@@ -225,7 +224,7 @@
# expect to see state ID address update on verify
expect(page).to have_text('test update address').once # only state id address update
# click update state id address
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
# check that the "No, I live at a different address" is checked"
@@ -247,7 +246,7 @@
expect(page).to have_content(t('headings.verify'))
expect(page).to have_current_path(idv_in_person_verify_info_path)
# click update state ID button on the verify page
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
# change address
@@ -262,7 +261,7 @@
# expect to see state ID address update on verify twice
expect(page).to have_text('test update address').twice # for state id addr and addr update
# click update state ID button on the verify page
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
# expect to be on the state ID page
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
expect(page).to have_checked_field(
@@ -426,7 +425,7 @@
expect(page).to have_text('PR')
# update state ID
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint'))
diff --git a/spec/features/idv/steps/in_person/verify_info_spec.rb b/spec/features/idv/steps/in_person/verify_info_spec.rb
index 051dd09b111..d56c3ca62aa 100644
--- a/spec/features/idv/steps/in_person/verify_info_spec.rb
+++ b/spec/features/idv/steps/in_person/verify_info_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
require 'axe-rspec'
-RSpec.describe 'doc auth IPP VerifyInfo', js: true, allowed_extra_analytics: [:*] do
+RSpec.describe 'doc auth IPP VerifyInfo', js: true do
include IdvStepHelper
include InPersonHelper
@@ -9,342 +9,170 @@
let(:fake_analytics) { FakeAnalytics.new(user: user) }
let(:enrollment) { InPersonEnrollment.new }
- context 'when in_person_state_id_controller_enabled is false' do
- before do
- allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
- allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).
- and_return(false)
- allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics)
- allow(user).to receive(:enrollment).
- and_return(enrollment)
- end
-
- it 'provides back buttons for address, state ID, and SSN that discard changes',
- allow_browser_log: true do
- sign_in_and_2fa_user(user)
- begin_in_person_proofing(user)
- complete_prepare_step(user)
- complete_location_step(user)
- complete_state_id_step(user)
- complete_ssn_step(user)
-
- # verify page
- expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_text(InPersonHelper::GOOD_FIRST_NAME)
- expect(page).to have_text(InPersonHelper::GOOD_LAST_NAME)
- expect(page).to have_text(InPersonHelper::GOOD_DOB_FORMATTED_EVENT)
- expect(page).to have_text(InPersonHelper::GOOD_STATE_ID_NUMBER)
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1).twice
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2).twice
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY).twice
- expect(page).to have_text(
- Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction],
- count: 3,
- )
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE).twice
- expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED)
-
- # click update state ID button
- click_button t('idv.buttons.change_state_id_label')
- expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
- fill_in t('in_person_proofing.form.state_id.first_name'), with: 'bad first name'
- click_doc_auth_back_link
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text(InPersonHelper::GOOD_FIRST_NAME)
- expect(page).not_to have_text('bad first name')
-
- # click update address link
- click_link t('idv.buttons.change_address_label')
- expect(page).to have_content(t('in_person_proofing.headings.update_address'))
- fill_in t('idv.form.address1'), with: 'bad address'
- click_doc_auth_back_link
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1)
- expect(page).not_to have_text('bad address')
-
- # click update ssn button
- click_on t('idv.buttons.change_ssn_label')
- expect(page).to have_content(t('doc_auth.headings.ssn_update'))
- fill_out_ssn_form_fail
- click_doc_auth_back_link
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED)
-
- complete_verify_step(user)
-
- # phone page
- expect(page).to have_content(t('titles.idv.phone'))
- end
-
- it 'returns the user to the verify info page when updates are made',
- allow_browser_log: true do
- sign_in_and_2fa_user(user)
- begin_in_person_proofing(user)
- complete_prepare_step(user)
- complete_location_step(user)
- complete_state_id_step(user)
- complete_ssn_step(user)
-
- # verify page
- expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text(InPersonHelper::GOOD_FIRST_NAME)
- expect(page).to have_text(InPersonHelper::GOOD_LAST_NAME)
- expect(page).to have_text(InPersonHelper::GOOD_DOB_FORMATTED_EVENT)
- expect(page).to have_text(InPersonHelper::GOOD_STATE_ID_NUMBER)
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1).twice
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2).twice
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY).twice
- expect(page).to have_text(
- Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction],
- count: 3,
- )
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE).twice
- expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED)
-
- # click update state ID button
- click_button t('idv.buttons.change_state_id_label')
- expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
- fill_in t('in_person_proofing.form.state_id.first_name'), with: 'Natalya'
- click_button t('forms.buttons.submit.update')
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text('Natalya')
- expect(page).not_to have_text('bad first name')
-
- # click update address link
- click_link t('idv.buttons.change_address_label')
- expect(page).to have_content(t('in_person_proofing.headings.update_address'))
- fill_in t('idv.form.address1'), with: '987 Fake St.'
- click_button t('forms.buttons.submit.update')
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text('987 Fake St.')
- expect(page).not_to have_text('bad address')
-
- # click update ssn button
- click_on t('idv.buttons.change_ssn_label')
- expect(page).to have_content(t('doc_auth.headings.ssn_update'))
- fill_in t('idv.form.ssn_label'), with: '900-12-2345'
- click_button t('forms.buttons.submit.update')
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text('9**-**-***5')
-
- complete_verify_step(user)
-
- # phone page
- expect(page).to have_content(t('titles.idv.phone'))
- end
-
- it 'does not proceed to the next page if resolution fails',
- allow_browser_log: true do
- sign_in_and_2fa_user
-
- begin_in_person_proofing(user)
- complete_prepare_step(user)
- complete_location_step(user)
- complete_state_id_step(user)
- fill_out_ssn_form_with_ssn_that_fails_resolution
- click_idv_continue
- complete_verify_step(user)
-
- expect(page).to have_current_path(idv_session_errors_warning_path(flow: 'in_person'))
- click_on t('idv.failure.button.warning')
-
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- end
-
- it 'proceeds to the next page if resolution passes',
- allow_browser_log: true do
- sign_in_and_2fa_user
- begin_in_person_proofing(user)
- complete_prepare_step(user)
- complete_location_step(user)
- complete_state_id_step(user)
- complete_ssn_step(user)
- complete_verify_step(user)
-
- expect(page).to have_content(t('titles.idv.phone'))
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth verify proofing results',
- hash_including(analytics_id: 'In Person Proofing'),
- )
- end
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
+ allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics)
+ allow(user).to receive(:enrollment).
+ and_return(enrollment)
end
- context 'when in_person_state_id_controller_enabled is true' do
- before do
- allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
- allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics)
- allow(user).to receive(:enrollment).
- and_return(enrollment)
- end
-
- it 'provides back buttons for address, state ID, and SSN that discard changes',
- allow_browser_log: true do
- sign_in_and_2fa_user(user)
- begin_in_person_proofing(user)
- complete_prepare_step(user)
- complete_location_step(user)
- complete_state_id_controller(user)
- complete_ssn_step(user)
-
- # verify page
- expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_text(InPersonHelper::GOOD_FIRST_NAME)
- expect(page).to have_text(InPersonHelper::GOOD_LAST_NAME)
- expect(page).to have_text(InPersonHelper::GOOD_DOB_FORMATTED_EVENT)
- expect(page).to have_text(InPersonHelper::GOOD_STATE_ID_NUMBER)
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1).twice
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2).twice
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY).twice
- expect(page).to have_text(
- Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction],
- count: 3,
- )
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE).twice
- expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED)
-
- # click update state ID button
- click_button t('idv.buttons.change_state_id_label')
- expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
- fill_in t('in_person_proofing.form.state_id.first_name'), with: 'bad first name'
- click_doc_auth_back_link
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text(InPersonHelper::GOOD_FIRST_NAME)
- expect(page).not_to have_text('bad first name')
-
- # click update address link
- click_link t('idv.buttons.change_address_label')
- expect(page).to have_content(t('in_person_proofing.headings.update_address'))
- fill_in t('idv.form.address1'), with: 'bad address'
- click_doc_auth_back_link
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1)
- expect(page).not_to have_text('bad address')
-
- # click update ssn button
- click_on t('idv.buttons.change_ssn_label')
- expect(page).to have_content(t('doc_auth.headings.ssn_update'))
- fill_out_ssn_form_fail
- click_doc_auth_back_link
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED)
-
- complete_verify_step(user)
-
- # phone page
- expect(page).to have_content(t('titles.idv.phone'))
- end
- it 'returns the user to the verify info page when updates are made',
- allow_browser_log: true do
- sign_in_and_2fa_user(user)
- begin_in_person_proofing(user)
- complete_prepare_step(user)
- complete_location_step(user)
- complete_state_id_controller(user)
- complete_ssn_step(user)
-
- # verify page
- expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text(InPersonHelper::GOOD_FIRST_NAME)
- expect(page).to have_text(InPersonHelper::GOOD_LAST_NAME)
- expect(page).to have_text(InPersonHelper::GOOD_DOB_FORMATTED_EVENT)
- expect(page).to have_text(InPersonHelper::GOOD_STATE_ID_NUMBER)
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1).twice
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2).twice
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY).twice
- expect(page).to have_text(
- Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction],
- count: 3,
- )
- expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE).twice
- expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED)
-
- # click update state ID button
- click_button t('idv.buttons.change_state_id_label')
- expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
- fill_in t('in_person_proofing.form.state_id.first_name'), with: 'Natalya'
- click_button t('forms.buttons.submit.update')
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text('Natalya')
- expect(page).not_to have_text('bad first name')
-
- # click update address link
- click_link t('idv.buttons.change_address_label')
- expect(page).to have_content(t('in_person_proofing.headings.update_address'))
- fill_in t('idv.form.address1'), with: '987 Fake St.'
- click_button t('forms.buttons.submit.update')
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text('987 Fake St.')
- expect(page).not_to have_text('bad address')
-
- # click update ssn button
- click_on t('idv.buttons.change_ssn_label')
- expect(page).to have_content(t('doc_auth.headings.ssn_update'))
- fill_in t('idv.form.ssn_label'), with: '900-12-2345'
- click_button t('forms.buttons.submit.update')
- expect(page).to have_content(t('headings.verify'))
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- expect(page).to have_text('9**-**-***5')
-
- complete_verify_step(user)
-
- # phone page
- expect(page).to have_content(t('titles.idv.phone'))
- end
+ it 'provides back buttons for address, state ID, and SSN that discard changes',
+ allow_browser_log: true do
+ sign_in_and_2fa_user(user)
+ begin_in_person_proofing(user)
+ complete_prepare_step(user)
+ complete_location_step(user)
+ complete_state_id_controller(user)
+ complete_ssn_step(user)
+
+ # verify page
+ expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info'))
+ expect(page).to have_current_path(idv_in_person_verify_info_path)
+ expect(page).to have_content(t('headings.verify'))
+ expect(page).to have_text(InPersonHelper::GOOD_FIRST_NAME)
+ expect(page).to have_text(InPersonHelper::GOOD_LAST_NAME)
+ expect(page).to have_text(InPersonHelper::GOOD_DOB_FORMATTED_EVENT)
+ expect(page).to have_text(InPersonHelper::GOOD_STATE_ID_NUMBER)
+ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1).twice
+ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2).twice
+ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY).twice
+ expect(page).to have_text(
+ Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction],
+ count: 3,
+ )
+ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE).twice
+ expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED)
+
+ # click update state ID button
+ click_link t('idv.buttons.change_state_id_label')
+ expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
+ fill_in t('in_person_proofing.form.state_id.first_name'), with: 'bad first name'
+ click_doc_auth_back_link
+ expect(page).to have_content(t('headings.verify'))
+ expect(page).to have_current_path(idv_in_person_verify_info_path)
+ expect(page).to have_text(InPersonHelper::GOOD_FIRST_NAME)
+ expect(page).not_to have_text('bad first name')
+
+ # click update address link
+ click_link t('idv.buttons.change_address_label')
+ expect(page).to have_content(t('in_person_proofing.headings.update_address'))
+ fill_in t('idv.form.address1'), with: 'bad address'
+ click_doc_auth_back_link
+ expect(page).to have_content(t('headings.verify'))
+ expect(page).to have_current_path(idv_in_person_verify_info_path)
+ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1)
+ expect(page).not_to have_text('bad address')
+
+ # click update ssn button
+ click_on t('idv.buttons.change_ssn_label')
+ expect(page).to have_content(t('doc_auth.headings.ssn_update'))
+ fill_out_ssn_form_fail
+ click_doc_auth_back_link
+ expect(page).to have_content(t('headings.verify'))
+ expect(page).to have_current_path(idv_in_person_verify_info_path)
+ expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED)
+
+ complete_verify_step(user)
+
+ # phone page
+ expect(page).to have_content(t('titles.idv.phone'))
+ end
- it 'does not proceed to the next page if resolution fails',
- allow_browser_log: true do
- sign_in_and_2fa_user
+ it 'returns the user to the verify info page when updates are made',
+ allow_browser_log: true do
+ sign_in_and_2fa_user(user)
+ begin_in_person_proofing(user)
+ complete_prepare_step(user)
+ complete_location_step(user)
+ complete_state_id_controller(user)
+ complete_ssn_step(user)
+
+ # verify page
+ expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info'))
+ expect(page).to have_current_path(idv_in_person_verify_info_path)
+ expect(page).to have_content(t('headings.verify'))
+ expect(page).to have_current_path(idv_in_person_verify_info_path)
+ expect(page).to have_text(InPersonHelper::GOOD_FIRST_NAME)
+ expect(page).to have_text(InPersonHelper::GOOD_LAST_NAME)
+ expect(page).to have_text(InPersonHelper::GOOD_DOB_FORMATTED_EVENT)
+ expect(page).to have_text(InPersonHelper::GOOD_STATE_ID_NUMBER)
+ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1).twice
+ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2).twice
+ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY).twice
+ expect(page).to have_text(
+ Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction],
+ count: 3,
+ )
+ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE).twice
+ expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED)
+
+ # click update state ID button
+ click_link t('idv.buttons.change_state_id_label')
+ expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
+ fill_in t('in_person_proofing.form.state_id.first_name'), with: 'Natalya'
+ click_button t('forms.buttons.submit.update')
+ expect(page).to have_content(t('headings.verify'))
+ expect(page).to have_current_path(idv_in_person_verify_info_path)
+ expect(page).to have_text('Natalya')
+ expect(page).not_to have_text('bad first name')
+
+ # click update address link
+ click_link t('idv.buttons.change_address_label')
+ expect(page).to have_content(t('in_person_proofing.headings.update_address'))
+ fill_in t('idv.form.address1'), with: '987 Fake St.'
+ click_button t('forms.buttons.submit.update')
+ expect(page).to have_content(t('headings.verify'))
+ expect(page).to have_current_path(idv_in_person_verify_info_path)
+ expect(page).to have_text('987 Fake St.')
+ expect(page).not_to have_text('bad address')
+
+ # click update ssn button
+ click_on t('idv.buttons.change_ssn_label')
+ expect(page).to have_content(t('doc_auth.headings.ssn_update'))
+ fill_in t('idv.form.ssn_label'), with: '900-12-2345'
+ click_button t('forms.buttons.submit.update')
+ expect(page).to have_content(t('headings.verify'))
+ expect(page).to have_current_path(idv_in_person_verify_info_path)
+ expect(page).to have_text('9**-**-***5')
+
+ complete_verify_step(user)
+
+ # phone page
+ expect(page).to have_content(t('titles.idv.phone'))
+ end
- begin_in_person_proofing(user)
- complete_prepare_step(user)
- complete_location_step(user)
- complete_state_id_controller(user)
- fill_out_ssn_form_with_ssn_that_fails_resolution
- click_idv_continue
- complete_verify_step(user)
+ it 'does not proceed to the next page if resolution fails',
+ allow_browser_log: true do
+ sign_in_and_2fa_user
- expect(page).to have_current_path(idv_session_errors_warning_path(flow: 'in_person'))
- click_on t('idv.failure.button.warning')
+ begin_in_person_proofing(user)
+ complete_prepare_step(user)
+ complete_location_step(user)
+ complete_state_id_controller(user)
+ fill_out_ssn_form_with_ssn_that_fails_resolution
+ click_idv_continue
+ complete_verify_step(user)
- expect(page).to have_current_path(idv_in_person_verify_info_path)
- end
+ expect(page).to have_current_path(idv_session_errors_warning_path(flow: 'in_person'))
+ click_on t('idv.failure.button.warning')
- it 'proceeds to the next page if resolution passes',
- allow_browser_log: true do
- sign_in_and_2fa_user
- begin_in_person_proofing(user)
- complete_prepare_step(user)
- complete_location_step(user)
- complete_state_id_controller(user)
- complete_ssn_step(user)
- complete_verify_step(user)
+ expect(page).to have_current_path(idv_in_person_verify_info_path)
+ end
- expect(page).to have_content(t('titles.idv.phone'))
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth verify proofing results',
- hash_including(analytics_id: 'In Person Proofing'),
- )
- end
+ it 'proceeds to the next page if resolution passes',
+ allow_browser_log: true do
+ sign_in_and_2fa_user
+ begin_in_person_proofing(user)
+ complete_prepare_step(user)
+ complete_location_step(user)
+ complete_state_id_controller(user)
+ complete_ssn_step(user)
+ complete_verify_step(user)
+
+ expect(page).to have_content(t('titles.idv.phone'))
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth verify proofing results',
+ hash_including(analytics_id: 'In Person Proofing'),
+ )
end
end
diff --git a/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb b/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb
index 9758d2a14a9..2a66b20974e 100644
--- a/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb
+++ b/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb
@@ -185,7 +185,7 @@
expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED)
# click update state ID button
- click_button t('idv.buttons.change_state_id_label')
+ click_link t('idv.buttons.change_state_id_label')
expect(page).to have_content(t('in_person_proofing.headings.update_state_id'))
choose t('in_person_proofing.form.state_id.same_address_as_id_yes')
click_button t('forms.buttons.submit.update')
diff --git a/spec/features/two_factor_authentication/piv_cac_sign_in_spec.rb b/spec/features/two_factor_authentication/piv_cac_sign_in_spec.rb
new file mode 100644
index 00000000000..6f00e16973a
--- /dev/null
+++ b/spec/features/two_factor_authentication/piv_cac_sign_in_spec.rb
@@ -0,0 +1,141 @@
+require 'rails_helper'
+
+RSpec.feature 'sign in with piv/cac' do
+ include SamlAuthHelper
+ include OidcAuthHelper
+
+ let(:user) { create(:user, :with_piv_or_cac, :with_phone) }
+
+ before do
+ sign_in_before_2fa(user)
+ end
+
+ context 'with piv/cac mismatch error' do
+ before do
+ stub_piv_cac_service(error: 'user.piv_cac_mismatch')
+ end
+
+ it 'allows a user to add a replacement piv after authenticating with another method' do
+ click_on t('forms.piv_cac_login.submit')
+ follow_piv_cac_redirect
+
+ expect(page).to have_current_path(login_two_factor_piv_cac_mismatch_path)
+ expect(page).to have_button(t('two_factor_authentication.piv_cac_mismatch.skip'))
+
+ click_on t('two_factor_authentication.piv_cac_mismatch.cta')
+
+ expect(page).to have_current_path(login_two_factor_options_path)
+ expect(page).to have_content(t('two_factor_authentication.piv_cac_mismatch.2fa_before_add'))
+ expect(page).to have_field(t('two_factor_authentication.login_options.sms'))
+ expect(page).to have_field(
+ t('two_factor_authentication.login_options.piv_cac'),
+ disabled: true,
+ )
+
+ select_2fa_option(:sms)
+ fill_in_code_with_last_phone_otp
+ click_submit_default
+
+ expect(page).to have_current_path(setup_piv_cac_path)
+ expect(page).to have_button(t('mfa.skip'))
+
+ stub_piv_cac_service
+ fill_in t('forms.totp_setup.totp_step_1'), with: 'New PIV'
+ click_on t('forms.piv_cac_setup.submit')
+ follow_piv_cac_redirect
+
+ expect(page).to have_current_path(account_path)
+ within(page.find('.card', text: t('headings.account.federal_employee_id'))) do
+ expect(page).to have_css('lg-manageable-authenticator', count: 2)
+ end
+ end
+
+ context 'with partner requiring piv/cac' do
+ before do
+ visit_idp_from_oidc_sp_with_hspd12_and_require_piv_cac
+ end
+
+ it 'does not allow a user to skip adding piv/cac' do
+ click_on t('forms.piv_cac_login.submit')
+ follow_piv_cac_redirect
+
+ expect(page).to have_current_path(login_two_factor_piv_cac_mismatch_path)
+ expect(page).not_to have_button(t('two_factor_authentication.piv_cac_mismatch.skip'))
+
+ click_on t('two_factor_authentication.piv_cac_mismatch.cta')
+
+ expect(page).to have_current_path(login_two_factor_options_path)
+ expect(page).to have_content(t('two_factor_authentication.piv_cac_mismatch.2fa_before_add'))
+ expect(page).to have_field(t('two_factor_authentication.login_options.sms'))
+ expect(page).to have_field(
+ t('two_factor_authentication.login_options.piv_cac'),
+ disabled: true,
+ )
+
+ select_2fa_option(:sms)
+ fill_in_code_with_last_phone_otp
+ click_submit_default
+
+ expect(page).to have_current_path(setup_piv_cac_path)
+ expect(page).not_to have_button(t('mfa.skip'))
+
+ stub_piv_cac_service
+ fill_in t('forms.totp_setup.totp_step_1'), with: 'New PIV'
+ click_on t('forms.piv_cac_setup.submit')
+ follow_piv_cac_redirect
+
+ expect(current_path).to eq(sign_up_completed_path)
+
+ click_agree_and_continue
+ expect(oidc_decoded_id_token[:x509_presented]).to eq(true)
+ expect(oidc_decoded_id_token[:x509_subject]).to be_present
+ end
+ end
+
+ context 'if the user chooses to skip adding piv/cac when prompted with mismatch' do
+ it 'allows the user to authenticate with another method' do
+ click_on t('forms.piv_cac_login.submit')
+ follow_piv_cac_redirect
+
+ expect(page).to have_current_path(login_two_factor_piv_cac_mismatch_path)
+ expect(page).to have_button(t('two_factor_authentication.piv_cac_mismatch.skip'))
+
+ click_on t('two_factor_authentication.piv_cac_mismatch.skip')
+
+ expect(page).to have_current_path(login_two_factor_options_path)
+ expect(page).not_to have_content(
+ t('two_factor_authentication.piv_cac_mismatch.2fa_before_add'),
+ )
+ expect(page).to have_field(t('two_factor_authentication.login_options.sms'))
+ expect(page).to have_field(
+ t('two_factor_authentication.login_options.piv_cac'),
+ disabled: true,
+ )
+
+ select_2fa_option(:sms)
+ fill_in_code_with_last_phone_otp
+ click_submit_default
+
+ expect(page).to have_current_path(account_path)
+ end
+ end
+
+ context 'with no other mfa methods available' do
+ let(:user) { create(:user, :with_piv_or_cac) }
+
+ it 'prompts the user to reset their account' do
+ click_on t('forms.piv_cac_login.submit')
+ follow_piv_cac_redirect
+
+ expect(page).to have_current_path(login_two_factor_piv_cac_mismatch_path)
+ expect(page).not_to have_button(t('two_factor_authentication.piv_cac_mismatch.cta'))
+ expect(page).not_to have_button(t('two_factor_authentication.piv_cac_mismatch.skip'))
+ expect(page).to have_link(t('two_factor_authentication.piv_cac_mismatch.delete_account'))
+
+ click_on t('two_factor_authentication.piv_cac_mismatch.delete_account')
+
+ expect(page).to have_current_path(account_reset_recovery_options_path)
+ end
+ end
+ end
+end
diff --git a/spec/features/two_factor_authentication/sign_in_spec.rb b/spec/features/two_factor_authentication/sign_in_spec.rb
index ee411103d0f..06f09bf1949 100644
--- a/spec/features/two_factor_authentication/sign_in_spec.rb
+++ b/spec/features/two_factor_authentication/sign_in_spec.rb
@@ -415,18 +415,6 @@ def attempt_to_bypass_2fa
expect(current_path).to eq account_path
end
- scenario 'user uses incorrect PIV/CAC as their second factor' do
- user = user_with_piv_cac
- sign_in_before_2fa(user)
- stub_piv_cac_service(uuid: Random.uuid)
-
- click_on t('forms.piv_cac_mfa.submit')
- follow_piv_cac_redirect
-
- expect(current_path).to eq login_two_factor_piv_cac_path
- expect(page).to have_content(t('two_factor_authentication.invalid_piv_cac'))
- end
-
context 'user with Voice preference sends SMS, causing a Telephony error' do
let(:user) do
create(
diff --git a/spec/jobs/fraud_rejection_daily_job_spec.rb b/spec/jobs/fraud_rejection_daily_job_spec.rb
index c131bcdc7f3..52bfbd1daa9 100644
--- a/spec/jobs/fraud_rejection_daily_job_spec.rb
+++ b/spec/jobs/fraud_rejection_daily_job_spec.rb
@@ -4,17 +4,16 @@
subject(:job) { FraudRejectionDailyJob.new }
let(:job_analytics) { FakeAnalytics.new }
- before do
- allow(job).to receive(:analytics).and_return(job_analytics)
- end
-
describe '#perform' do
it 'rejects profiles which have been review pending for more than 30 days' do
- create(:profile, fraud_review_pending_at: 31.days.ago)
+ rejectedable_profile = create(:profile, fraud_review_pending_at: 31.days.ago)
create(:profile, fraud_review_pending_at: 20.days.ago)
rejected_profiles = Profile.where.not(fraud_rejection_at: nil)
+ allow(job).to receive(:analytics).with(user: rejectedable_profile.user).
+ and_return(job_analytics)
+
expect { job.perform(Time.zone.today) }.to change { rejected_profiles.count }.by(1)
expect(job_analytics).to have_logged_event(
'Fraud: Automatic Fraud Rejection',
diff --git a/spec/lib/data_pull_spec.rb b/spec/lib/data_pull_spec.rb
index de649d74f3f..640f0741544 100644
--- a/spec/lib/data_pull_spec.rb
+++ b/spec/lib/data_pull_spec.rb
@@ -458,8 +458,8 @@
expect(result.table).to match_array(
[
%w[uuid date events_count],
- [user.uuid, '2023-01-02', 5],
- [user.uuid, '2023-01-01', 1],
+ [user.uuid, Date.new(2023, 1, 2), 5],
+ [user.uuid, Date.new(2023, 1, 1), 1],
['uuid-does-not-exist', '[UUID NOT FOUND]', nil],
],
)
diff --git a/spec/lib/reporting/identity_verification_report_spec.rb b/spec/lib/reporting/identity_verification_report_spec.rb
index e7c96ac53bf..2f5f3e3ae68 100644
--- a/spec/lib/reporting/identity_verification_report_spec.rb
+++ b/spec/lib/reporting/identity_verification_report_spec.rb
@@ -2,66 +2,104 @@
require 'reporting/identity_verification_report'
RSpec.describe Reporting::IdentityVerificationReport do
- let(:issuer) { 'my:example:issuer' }
+ let(:issuer) { nil }
let(:time_range) { Date.new(2022, 1, 1).all_day }
+ let(:cloudwatch_logs) do
+ [
+ # Online verification user (failed each vendor once, then succeeded once)
+ { 'user_id' => 'user1', 'name' => 'IdV: doc auth welcome visited' },
+ { 'user_id' => 'user1', 'name' => 'IdV: doc auth welcome submitted' },
+ { 'user_id' => 'user1',
+ 'name' => 'IdV: doc auth image upload vendor submitted',
+ 'doc_auth_failed_non_fraud' => '1' },
+ { 'user_id' => 'user1',
+ 'name' => 'IdV: doc auth image upload vendor submitted',
+ 'success' => '1' },
+ { 'user_id' => 'user1', 'name' => 'IdV: doc auth verify proofing results', 'success' => '0' },
+ { 'user_id' => 'user1', 'name' => 'IdV: doc auth verify proofing results', 'success' => '1' },
+ { 'user_id' => 'user1', 'name' => 'IdV: phone confirmation vendor', 'success' => '0' },
+ { 'user_id' => 'user1', 'name' => 'IdV: phone confirmation vendor', 'success' => '1' },
+ { 'user_id' => 'user1', 'name' => 'IdV: final resolution', 'identity_verified' => '1' },
+
+ # Letter requested user (incomplete)
+ { 'user_id' => 'user2', 'name' => 'IdV: doc auth welcome visited' },
+ { 'user_id' => 'user2', 'name' => 'IdV: doc auth welcome submitted' },
+ { 'user_id' => 'user2',
+ 'name' => 'IdV: doc auth image upload vendor submitted',
+ 'success' => '1' },
+ { 'user_id' => 'user2',
+ 'name' => 'IdV: final resolution',
+ 'gpo_verification_pending' => '1' },
+
+ # Fraud review passed user
+ { 'user_id' => 'user3', 'name' => 'IdV: doc auth welcome visited' },
+ { 'user_id' => 'user3', 'name' => 'IdV: doc auth welcome submitted' },
+ { 'user_id' => 'user3',
+ 'name' => 'IdV: doc auth image upload vendor submitted',
+ 'success' => '1' },
+ { 'user_id' => 'user3', 'name' => 'IdV: final resolution', 'fraud_review_pending' => '1' },
+ { 'user_id' => 'user3', 'name' => 'Fraud: Profile review passed', 'success' => '1' },
+
+ # GPO confirmation followed by passing fraud review
+ { 'user_id' => 'user4', 'name' => 'IdV: GPO verification submitted' },
+ { 'user_id' => 'user4', 'name' => 'Fraud: Profile review passed', 'success' => '1' },
+
+ # Success through in-person verification, failed doc auth (rejected)
+ { 'user_id' => 'user5', 'name' => 'IdV: doc auth welcome visited' },
+ { 'user_id' => 'user5', 'name' => 'IdV: doc auth welcome submitted' },
+ { 'user_id' => 'user5',
+ 'name' => 'IdV: doc auth image upload vendor submitted',
+ 'doc_auth_failed_non_fraud' => '1' },
+ { 'user_id' => 'user5',
+ 'name' => 'IdV: final resolution',
+ 'in_person_verification_pending' => '1' },
+ { 'user_id' => 'user5', 'name' => 'GetUspsProofingResultsJob: Enrollment status updated' },
+
+ # Incomplete user
+ { 'user_id' => 'user6', 'name' => 'IdV: doc auth welcome visited' },
+ { 'user_id' => 'user6', 'name' => 'IdV: doc auth welcome submitted' },
+ { 'user_id' => 'user6',
+ 'name' => 'IdV: doc auth image upload vendor submitted',
+ 'doc_auth_failed_non_fraud' => '1' },
+
+ # Fraud review user (rejected)
+ { 'user_id' => 'user7', 'name' => 'IdV: doc auth welcome visited' },
+ { 'user_id' => 'user7', 'name' => 'IdV: doc auth welcome submitted' },
+ { 'user_id' => 'user7',
+ 'name' => 'IdV: doc auth image upload vendor submitted',
+ 'success' => '1' },
+ { 'user_id' => 'user7', 'name' => 'IdV: final resolution', 'fraud_review_pending' => '1' },
+ { 'user_id' => 'user7', 'name' => 'Fraud: Profile review rejected', 'success' => '1' },
+
+ # Just fraud rejection
+ { 'user_id' => 'user8', 'name' => 'Fraud: Profile review rejected', 'success' => '1' },
+
+ # GPO confirmation followed by fraud rejection
+ { 'user_id' => 'user9',
+ 'name' => 'IdV: GPO verification submitted',
+ 'fraud_review_pending' => '1' },
+ { 'user_id' => 'user9', 'name' => 'Fraud: Profile review rejected', 'success' => '1' },
+
+ # IPP user in fraud review queue who is then passed
+ {
+ 'user_id' => 'user10',
+ 'name' => 'GetUspsProofingResultsJob: Enrollment status updated',
+ 'fraud_review_pending' => '1',
+ },
+ { 'user_id' => 'user10', 'name' => 'Fraud: Profile review passed', 'success' => '1' },
+
+ # User who bounced on welcome screen
+ { 'user_id' => 'user11', 'name' => 'IdV: doc auth welcome visited' },
+ ]
+ end
+
subject(:report) do
Reporting::IdentityVerificationReport.new(issuers: Array(issuer), time_range:)
end
- # rubocop:disable Layout/LineLength
before do
- stub_cloudwatch_logs(
- [
- # Online verification user (failed each vendor once, then succeeded once)
- { 'user_id' => 'user1', 'name' => 'IdV: doc auth welcome visited' },
- { 'user_id' => 'user1', 'name' => 'IdV: doc auth welcome submitted' },
- { 'user_id' => 'user1', 'name' => 'IdV: doc auth image upload vendor submitted', 'doc_auth_failed_non_fraud' => '1' },
- { 'user_id' => 'user1', 'name' => 'IdV: doc auth image upload vendor submitted', 'success' => '1' },
- { 'user_id' => 'user1', 'name' => 'IdV: doc auth verify proofing results', 'success' => '0' },
- { 'user_id' => 'user1', 'name' => 'IdV: doc auth verify proofing results', 'success' => '1' },
- { 'user_id' => 'user1', 'name' => 'IdV: phone confirmation vendor', 'success' => '0' },
- { 'user_id' => 'user1', 'name' => 'IdV: phone confirmation vendor', 'success' => '1' },
- { 'user_id' => 'user1', 'name' => 'IdV: final resolution', 'identity_verified' => '1' },
-
- # Letter requested user (incomplete)
- { 'user_id' => 'user2', 'name' => 'IdV: doc auth welcome visited' },
- { 'user_id' => 'user2', 'name' => 'IdV: doc auth welcome submitted' },
- { 'user_id' => 'user2', 'name' => 'IdV: doc auth image upload vendor submitted', 'success' => '1' },
- { 'user_id' => 'user2', 'name' => 'IdV: final resolution', 'gpo_verification_pending' => '1' },
-
- # Fraud review passed user
- { 'user_id' => 'user3', 'name' => 'IdV: doc auth welcome visited' },
- { 'user_id' => 'user3', 'name' => 'IdV: doc auth welcome submitted' },
- { 'user_id' => 'user3', 'name' => 'IdV: doc auth image upload vendor submitted', 'success' => '1' },
- { 'user_id' => 'user3', 'name' => 'IdV: final resolution', 'fraud_review_pending' => '1' },
- { 'user_id' => 'user3', 'name' => 'Fraud: Profile review passed', 'success' => '1' },
-
- # Success through address confirmation user
- { 'user_id' => 'user4', 'name' => 'IdV: GPO verification submitted' },
- { 'user_id' => 'user4', 'name' => 'Fraud: Profile review passed', 'success' => '1' },
-
- # Success through in-person verification, failed doc auth (rejected)
- { 'user_id' => 'user5', 'name' => 'IdV: doc auth welcome visited' },
- { 'user_id' => 'user5', 'name' => 'IdV: doc auth welcome submitted' },
- { 'user_id' => 'user5', 'name' => 'IdV: doc auth image upload vendor submitted', 'doc_auth_failed_non_fraud' => '1' },
- { 'user_id' => 'user5', 'name' => 'IdV: final resolution', 'in_person_verification_pending' => '1' },
- { 'user_id' => 'user5', 'name' => 'GetUspsProofingResultsJob: Enrollment status updated' },
-
- # Incomplete user
- { 'user_id' => 'user6', 'name' => 'IdV: doc auth welcome visited' },
- { 'user_id' => 'user6', 'name' => 'IdV: doc auth welcome submitted' },
- { 'user_id' => 'user6', 'name' => 'IdV: doc auth image upload vendor submitted', 'doc_auth_failed_non_fraud' => '1' },
-
- # Fraud review user (rejected)
- { 'user_id' => 'user7', 'name' => 'IdV: doc auth welcome visited' },
- { 'user_id' => 'user7', 'name' => 'IdV: doc auth welcome submitted' },
- { 'user_id' => 'user7', 'name' => 'IdV: doc auth image upload vendor submitted', 'success' => '1' },
- { 'user_id' => 'user7', 'name' => 'IdV: final resolution', 'fraud_review_pending' => '1' },
- { 'user_id' => 'user7', 'name' => 'Fraud: Profile review rejected', 'success' => '1' },
-
- ],
- )
+ stub_cloudwatch_logs(cloudwatch_logs)
end
describe '#as_csv' do
@@ -69,11 +107,11 @@
expected_csv = [
['Report Timeframe', "#{time_range.begin} to #{time_range.end}"],
['Report Generated', Date.today.to_s], # rubocop:disable Rails/Date
- ['Issuer', issuer],
+ issuer && ['Issuer', issuer],
[],
['Metric', '# of Users'],
[],
- ['IDV started', 6],
+ ['IDV started', 7],
['Welcome Submitted', 6],
['Image Submitted', 6],
[],
@@ -87,19 +125,17 @@
['Workflow completed - GPO + In-Person Pending', 0],
['Workflow completed - GPO + In-Person Pending - Fraud Review', 0],
[],
- ['Fraud review rejected', 1],
- ['Successfully Verified', 4],
+ ['Fraud review rejected', 3],
+ ['Successfully Verified', 5],
['Successfully Verified - With phone number', 1],
['Successfully Verified - With mailed code', 1],
['Successfully Verified - In Person', 1],
- ['Successfully Verified - Passed fraud review', 2],
- ['Blanket Proofing Rate (IDV Started to Successfully Verified)', 0.6666666666666666],
- ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', 0.6666666666666666],
- ['Actual Proofing Rate (Image Submitted to Successfully Verified)', 0.6666666666666666],
- ['Industry Proofing Rate (Verified minus IDV Rejected)', 0.8],
- ]
- # rubocop:enable Layout/LineLength
-
+ ['Successfully Verified - Passed fraud review', 3],
+ ['Blanket Proofing Rate (IDV Started to Successfully Verified)', (5.0 / 7.0)],
+ ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', (5.0 / 6.0)],
+ ['Actual Proofing Rate (Image Submitted to Successfully Verified)', (5.0 / 6.0)],
+ ['Industry Proofing Rate (Verified minus IDV Rejected)', (5.0 / 6.0)],
+ ].compact
aggregate_failures do
report.as_csv.zip(expected_csv).each do |actual, expected|
expect(actual).to eq(expected)
@@ -115,11 +151,11 @@
expected_csv = [
['Report Timeframe', "#{time_range.begin} to #{time_range.end}"],
['Report Generated', Date.today.to_s], # rubocop:disable Rails/Date
- ['Issuer', issuer],
+ issuer && ['Issuer', issuer],
[],
['Metric', '# of Users'],
[],
- ['IDV started', '6'],
+ ['IDV started', '7'],
['Welcome Submitted', '6'],
['Image Submitted', '6'],
[],
@@ -133,17 +169,17 @@
['Workflow completed - GPO + In-Person Pending', '0'],
['Workflow completed - GPO + In-Person Pending - Fraud Review', '0'],
[],
- ['Fraud review rejected', '1'],
- ['Successfully Verified', '4'],
+ ['Fraud review rejected', '3'],
+ ['Successfully Verified', '5'],
['Successfully Verified - With phone number', '1'],
['Successfully Verified - With mailed code', '1'],
['Successfully Verified - In Person', '1'],
- ['Successfully Verified - Passed fraud review', '2'],
- ['Blanket Proofing Rate (IDV Started to Successfully Verified)', '0.6666666666666666'],
- ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', '0.6666666666666666'],
- ['Actual Proofing Rate (Image Submitted to Successfully Verified)', '0.6666666666666666'],
- ['Industry Proofing Rate (Verified minus IDV Rejected)', '0.8'],
- ]
+ ['Successfully Verified - Passed fraud review', '3'],
+ ['Blanket Proofing Rate (IDV Started to Successfully Verified)', (5.0 / 7.0).to_s],
+ ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', (5.0 / 6.0).to_s],
+ ['Actual Proofing Rate (Image Submitted to Successfully Verified)', (5.0 / 6.0).to_s],
+ ['Industry Proofing Rate (Verified minus IDV Rejected)', (5.0 / 6.0).to_s],
+ ].compact
aggregate_failures do
csv.map(&:to_a).zip(expected_csv).each do |actual, expected|
@@ -161,7 +197,7 @@
'IdV: doc auth image upload vendor submitted' => 6,
'IdV: doc auth verify proofing results' => 1,
'IdV: doc auth welcome submitted' => 6,
- 'IdV: doc auth welcome visited' => 6,
+ 'IdV: doc auth welcome visited' => 7,
'IdV: final resolution' => 5,
'IdV: GPO verification submitted' => 1,
'IdV: phone confirmation vendor' => 1,
@@ -174,10 +210,30 @@
'IdV Reject: Doc Auth' => 3,
'IdV Reject: Phone Finder' => 1,
'IdV Reject: Verify' => 1,
- 'Fraud: Profile review passed' => 2,
- 'Fraud: Profile review rejected' => 1,
+ 'Fraud: Profile review passed' => 3,
+ 'Fraud: Profile review rejected' => 3,
)
end
+
+ context 'when an issuer is specified' do
+ let(:issuer) { 'my:example:issuer' }
+
+ context 'and events have service provider data' do
+ let(:cloudwatch_logs) do
+ super().map do |event|
+ event.merge(
+ 'service_provider' => issuer,
+ )
+ end
+ end
+
+ it 'includes users per sp' do
+ expect(report.data.transform_values(&:count)).to include(
+ 'sp:my:example:issuer' => 11,
+ )
+ end
+ end
+ end
end
describe '#idv_doc_auth_rejected' do
@@ -186,6 +242,210 @@
end
end
+ describe '#fraud_review_passed' do
+ let(:service_provider_for_non_fraud_events) { nil }
+ let(:service_provider_for_fraud_events) { nil }
+
+ let(:cloudwatch_logs) do
+ super().map do |event|
+ is_fraud_event = event['name'].include?('Fraud')
+ event.merge(
+ 'service_provider' => is_fraud_event ?
+ service_provider_for_fraud_events :
+ service_provider_for_non_fraud_events,
+ )
+ end
+ end
+
+ context 'when an issuer is specified' do
+ let(:issuer) { 'my:example:issuer' }
+
+ context 'and fraud events are not tagged with sp information' do
+ context 'but other events are tagged for the sp' do
+ let(:service_provider_for_non_fraud_events) { issuer }
+ it 'is users who completed workflow + passed fraud review and any event matches issuer' do
+ expect(report.fraud_review_passed).to eql(3)
+ end
+ end
+
+ context 'and other events are not tagged with an sp' do
+ it 'does not include any users' do
+ expect(report.fraud_review_passed).to eql(0)
+ end
+ end
+
+ context 'and other events are tagged for a different sp' do
+ let(:service_provider_for_non_fraud_events) { 'some:other:sp' }
+
+ it 'does not include any users' do
+ expect(report.fraud_review_passed).to eql(0)
+ end
+ end
+ end
+ context 'and fraud events are tagged with sp information' do
+ let(:service_provider_for_fraud_events) { issuer }
+
+ context 'but other events are not tagged at all' do
+ it 'counts all fraud events tagged for the sp' do
+ expect(report.fraud_review_passed).to eql(3)
+ end
+ end
+
+ context 'but other events are not tagged with the same SP' do
+ let(:service_provider_for_non_fraud_events) { 'some:other:sp' }
+ it 'still counts all fraud events tagged for the sp' do
+ expect(report.fraud_review_passed).to eql(3)
+ end
+ end
+
+ context 'but the fraud events are tagged for the wrong sp' do
+ let(:service_provider_for_fraud_events) { 'some:other:sp' }
+
+ context 'and other events are not tagged' do
+ it 'does not find any users' do
+ expect(report.fraud_review_passed).to eql(0)
+ end
+ end
+
+ context 'and other events are tagged for the right sp' do
+ let(:service_provider_for_non_fraud_events) { issuer }
+ it 'still finds those users' do
+ expect(report.fraud_review_passed).to eql(3)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when an issuer is not specified' do
+ let(:issuer) { nil }
+
+ it 'includes users who did not complete workflow and passed fraud review' do
+ expect(report.fraud_review_passed).to eql(3)
+ end
+ end
+ end
+
+ describe '#idv_fraud_rejected' do
+ let(:service_provider_for_non_fraud_events) { nil }
+ let(:service_provider_for_fraud_events) { nil }
+
+ let(:cloudwatch_logs) do
+ super().map do |event|
+ is_fraud_event = event['name'].include?('Fraud')
+ event.merge(
+ 'service_provider' => is_fraud_event ?
+ service_provider_for_fraud_events :
+ service_provider_for_non_fraud_events,
+ )
+ end
+ end
+
+ context 'when an issuer is specified' do
+ let(:issuer) { 'my:example:issuer' }
+
+ context 'and fraud events are not tagged with sp information' do
+ context 'but other events are tagged for the sp' do
+ let(:service_provider_for_non_fraud_events) { issuer }
+ it 'is the count of fraud rejected users where any other event matches on issuer' do
+ expect(report.idv_fraud_rejected).to eql(2)
+ end
+ end
+
+ context 'and other events are not tagged with an sp' do
+ it 'does not include any users' do
+ expect(report.idv_fraud_rejected).to eql(0)
+ end
+ end
+
+ context 'and other events are tagged for a different sp' do
+ let(:service_provider_for_non_fraud_events) { 'some:other:sp' }
+
+ it 'does not include any users' do
+ expect(report.idv_fraud_rejected).to eql(0)
+ end
+ end
+ end
+
+ context 'and fraud events are tagged with sp information' do
+ let(:service_provider_for_fraud_events) { issuer }
+
+ context 'but other events are not tagged at all' do
+ it 'counts all fraud events tagged for the sp' do
+ expect(report.idv_fraud_rejected).to eql(3)
+ end
+ end
+
+ context 'but other events are not tagged with the same SP' do
+ let(:service_provider_for_non_fraud_events) { 'some:other:sp' }
+ it 'still counts all fraud events tagged for the sp' do
+ expect(report.idv_fraud_rejected).to eql(3)
+ end
+ end
+
+ context 'but the fraud events are tagged for the wrong sp' do
+ let(:service_provider_for_fraud_events) { 'some:other:sp' }
+
+ context 'and other events are not tagged' do
+ it 'does not find any users' do
+ expect(report.idv_fraud_rejected).to eql(0)
+ end
+ end
+
+ context 'and other events are tagged for the right sp' do
+ let(:service_provider_for_non_fraud_events) { issuer }
+ it 'still finds those users' do
+ expect(report.idv_fraud_rejected).to eql(2)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when an issuer is not specified' do
+ let(:issuer) { nil }
+
+ it 'includes users who did not complete workflow and failed fraud review' do
+ expect(report.idv_fraud_rejected).to eql(3)
+ end
+ end
+ end
+
+ describe '#successfully_verified_users' do
+ it 'is the count of users who verified or passed fraud review' do
+ expect(report.successfully_verified_users).to eql(5)
+ end
+
+ context 'multiple issuers specified' do
+ let(:cloudwatch_logs) do
+ [
+ { 'user_id' => 'user1',
+ 'name' => 'IdV: final resolution',
+ 'identity_verified' => '1',
+ 'service_provider' => 'issuer1' },
+ { 'user_id' => 'user2',
+ 'name' => 'IdV: final resolution',
+ 'identity_verified' => '1',
+ 'service_provider' => 'issuer2' },
+ { 'user_id' => 'user4',
+ 'name' => 'IdV: final resolution',
+ 'fraud_review_pending' => '1',
+ 'service_provider' => 'issuer1' },
+ { 'user_id' => 'user4', 'name' => 'Fraud: Profile review passed', 'success' => '1' },
+ { 'user_id' => 'user5',
+ 'name' => 'IdV: final resolution',
+ 'fraud_review_pending' => '1',
+ 'service_provider' => 'issuer2' },
+ { 'user_id' => 'user5', 'name' => 'Fraud: Profile review rejected', 'success' => '1' },
+ ]
+ end
+
+ it 'combines fraud results across issuers' do
+ expect(report.successfully_verified_users).to eql(3)
+ end
+ end
+ end
+
describe '#merge', :freeze_time do
it 'makes a new instance with merged data' do
report1 = Reporting::IdentityVerificationReport.new(
@@ -223,10 +483,12 @@
describe '#query' do
context 'with an issuer' do
+ let(:issuer) { 'my:example:issuer' }
+
it 'includes an issuer filter' do
result = subject.query
- expect(result).to include('| filter properties.service_provider IN ["my:example:issuer"]')
+ expect(result).to include('| filter service_provider IN ["my:example:issuer"]')
end
end
@@ -242,12 +504,43 @@
it 'includes GPO submission events with old name' do
expected = <<~FRAGMENT
- | filter (name in ["IdV: enter verify by mail code submitted","IdV: GPO verification submitted"] and properties.event_properties.success = 1 and !properties.event_properties.pending_in_person_enrollment and !properties.event_properties.fraud_check_failed)
+ | filter (name in ["IdV: enter verify by mail code submitted","IdV: GPO verification submitted"] and properties.event_properties.success = 1 and !properties.event_properties.pending_in_person_enrollment)
or (name not in ["IdV: enter verify by mail code submitted","IdV: GPO verification submitted"])
FRAGMENT
expect(subject.query).to include(expected)
end
+
+ it 'normalizes different attributes into fraud_review_pending' do
+ # rubocop:disable Layout/LineLength
+ elements = [
+ 'coalesce(properties.event_properties.fraud_review_pending, 0)',
+ '!isblank(properties.event_properties.fraud_pending_reason)',
+ 'coalesce(properties.event_properties.fraud_check_failed, 0)',
+ 'coalesce((ispresent(properties.event_properties.tmx_status) and properties.event_properties.tmx_status in ["threatmetrix_review", "threatmetrix_reject"]), 0)',
+ ]
+ # rubocop:enable Layout/LineLength
+
+ expected = <<~FRAGMENT
+ (#{elements.join(" OR ")}) AS fraud_review_pending
+ FRAGMENT
+ expect(subject.query).to include(expected)
+ end
+
+ it 'includes IPP events without regard to tmx_status' do
+ expected = <<~FRAGMENT
+ | filter (name = "GetUspsProofingResultsJob: Enrollment status updated" and properties.event_properties.passed = 1)
+ or (name != "GetUspsProofingResultsJob: Enrollment status updated")
+ FRAGMENT
+ expect(subject.query).to include(expected)
+ end
+
+ it 'accepts properties.event_properties.issuer as service_provider' do
+ expected = <<~FRAGMENT
+ coalesce(properties.service_provider, properties.event_properties.issuer) AS service_provider
+ FRAGMENT
+ expect(subject.query).to include(expected)
+ end
end
describe '#cloudwatch_client' do
diff --git a/spec/lib/tasks/check_for_pending_migrations_rake_spec.rb b/spec/lib/tasks/check_for_pending_migrations_rake_spec.rb
new file mode 100644
index 00000000000..eb20f882db4
--- /dev/null
+++ b/spec/lib/tasks/check_for_pending_migrations_rake_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+require 'rake'
+
+RSpec.describe 'check for pending migrations rake tasks' do
+ before do
+ Rake.application.rake_require 'tasks/check_for_pending_migrations'
+ Rake::Task.define_task(:environment)
+ end
+
+ describe 'db:check_for_pending_migrations' do
+ it 'runs successfully' do
+ Rake::Task['db:check_for_pending_migrations'].invoke
+ end
+ end
+end
diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb
index ec81d914965..e5c2f96a542 100644
--- a/spec/mailers/previews/user_mailer_preview.rb
+++ b/spec/mailers/previews/user_mailer_preview.rb
@@ -145,7 +145,12 @@ def add_email_associated_with_another_account
end
def account_verified
- service_provider = ServiceProvider.find_by(friendly_name: 'Example Sinatra App')
+ service_provider = unsaveable(
+ ServiceProvider.new(
+ friendly_name: 'Example Sinatra App',
+ return_to_sp_url: 'http://example.com',
+ ),
+ )
UserMailer.with(user: user, email_address: email_address_record).account_verified(
profile: unsaveable(
Profile.new(
diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb
index 87337726d98..59f25d32c69 100644
--- a/spec/mailers/user_mailer_spec.rb
+++ b/spec/mailers/user_mailer_spec.rb
@@ -567,6 +567,37 @@ def expect_email_body_to_have_help_and_contact_links
end
let(:visited_location_name) { 'ACQUAINTANCESHIP' }
+ describe '#in_person_deadline_passed' do
+ let(:enrollment) do
+ create(
+ :in_person_enrollment,
+ :expired,
+ selected_location_details: { name: 'FRIENDSHIP' },
+ )
+ end
+
+ let(:mail) do
+ UserMailer.with(user: user, email_address: email_address).in_person_deadline_passed(
+ enrollment: enrollment,
+ visited_location_name: visited_location_name,
+ )
+ end
+
+ it_behaves_like 'a system email'
+ it_behaves_like 'an email that respects user email locale preference'
+
+ context 'when the keyword argument visited_location_name is missing' do
+ let(:mail) do
+ UserMailer.with(user: user, email_address: email_address).in_person_deadline_passed(
+ enrollment: enrollment,
+ )
+ end
+ it 'sends the email successfully' do
+ mail.deliver_later
+ end
+ end
+ end
+
describe '#in_person_ready_to_verify' do
let(:mail) do
UserMailer.with(user: user, email_address: email_address).in_person_ready_to_verify(
@@ -882,6 +913,17 @@ def expect_email_body_to_have_help_and_contact_links
it_behaves_like 'a system email'
it_behaves_like 'an email that respects user email locale preference'
+
+ context 'when the keyword argument visited_location_name is missing' do
+ let(:mail) do
+ UserMailer.with(user: user, email_address: email_address).in_person_verified(
+ enrollment: enrollment,
+ )
+ end
+ it 'sends the email successfully' do
+ mail.deliver_later
+ end
+ end
end
describe '#in_person_failed' do
@@ -903,6 +945,17 @@ def expect_email_body_to_have_help_and_contact_links
it_behaves_like 'a system email'
it_behaves_like 'an email that respects user email locale preference'
+
+ context 'when the keyword argument visited_location_name is missing' do
+ let(:mail) do
+ UserMailer.with(user: user, email_address: email_address).in_person_failed(
+ enrollment: enrollment,
+ )
+ end
+ it 'sends the email successfully' do
+ mail.deliver_later
+ end
+ end
end
describe '#in_person_failed_fraud' do
@@ -923,6 +976,17 @@ def expect_email_body_to_have_help_and_contact_links
it_behaves_like 'a system email'
it_behaves_like 'an email that respects user email locale preference'
+
+ context 'when the keyword argument visited_location_name is missing' do
+ let(:mail) do
+ UserMailer.with(user: user, email_address: email_address).in_person_failed_fraud(
+ enrollment: enrollment,
+ )
+ end
+ it 'sends the email successfully' do
+ mail.deliver_later
+ end
+ end
end
describe '#in_person_please_call' do
@@ -935,6 +999,17 @@ def expect_email_body_to_have_help_and_contact_links
it_behaves_like 'a system email'
it_behaves_like 'an email that respects user email locale preference'
+
+ context 'when the keyword argument visited_location_name is missing' do
+ let(:mail) do
+ UserMailer.with(user: user, email_address: email_address).in_person_please_call(
+ enrollment: enrollment,
+ )
+ end
+ it 'sends the email successfully' do
+ mail.deliver_later
+ end
+ end
end
describe '#in_person_completion_survey' do
diff --git a/spec/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter_spec.rb
index e2d6d5fa8bb..274044ff3da 100644
--- a/spec/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter_spec.rb
+++ b/spec/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter_spec.rb
@@ -14,6 +14,43 @@
end
end
+ describe '#render_in' do
+ let(:user_session) { {} }
+ let(:view_context) { ActionController::Base.new.view_context }
+
+ before do
+ allow(view_context).to receive(:user_session).and_return(user_session)
+ end
+
+ it 'assigns disabled instance variable to false ahead of capture' do
+ expect(view_context).to receive(:capture) do
+ expect(presenter.instance_variable_get(:@disabled)).to eq(false)
+ end
+
+ presenter.render_in(view_context)
+ end
+
+ it 'renders captured block content' do
+ expect(view_context).to receive(:capture) do |*_args, &block|
+ expect(block.call).to eq('content')
+ end
+
+ presenter.render_in(view_context) { 'content' }
+ end
+
+ context 'when view context user session incudes add_piv_cac_after_2fa key' do
+ let(:user_session) { { add_piv_cac_after_2fa: :anything } }
+
+ it 'assigns disabled instance variable to true ahead of capture' do
+ expect(view_context).to receive(:capture) do
+ expect(presenter.instance_variable_get(:@disabled)).to eq(true)
+ end
+
+ presenter.render_in(view_context)
+ end
+ end
+ end
+
describe '#label' do
it 'returns the label text' do
expect(presenter.label).to eq(
@@ -29,4 +66,24 @@
)
end
end
+
+ describe '#disabled?' do
+ subject(:disabled?) { presenter.disabled? }
+
+ context 'when disabled instance variable is unassigned' do
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when disabled instance variable is false' do
+ before { presenter.instance_variable_set(:@disabled, false) }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when disabled instance variable is true' do
+ before { presenter.instance_variable_set(:@disabled, true) }
+
+ it { is_expected.to eq(true) }
+ end
+ end
end
diff --git a/spec/presenters/two_factor_login_options_presenter_spec.rb b/spec/presenters/two_factor_login_options_presenter_spec.rb
index 14da580896f..c6f02a6211c 100644
--- a/spec/presenters/two_factor_login_options_presenter_spec.rb
+++ b/spec/presenters/two_factor_login_options_presenter_spec.rb
@@ -9,15 +9,17 @@
let(:piv_cac_required) { false }
let(:reauthentication_context) { false }
let(:service_provider) { nil }
+ let(:add_piv_cac_after_2fa) { false }
subject(:presenter) do
TwoFactorLoginOptionsPresenter.new(
- user: user,
- view: view,
- reauthentication_context: reauthentication_context,
- service_provider: service_provider,
- phishing_resistant_required: phishing_resistant_required,
- piv_cac_required: piv_cac_required,
+ user:,
+ view:,
+ reauthentication_context:,
+ service_provider:,
+ phishing_resistant_required:,
+ piv_cac_required:,
+ add_piv_cac_after_2fa:,
)
end
@@ -47,6 +49,12 @@
it { should eq t('two_factor_authentication.login_intro') }
end
+ context 'add piv cac after 2fa' do
+ let(:add_piv_cac_after_2fa) { true }
+
+ it { should eq t('two_factor_authentication.login_intro') }
+ end
+
context 'reauthentication user session context' do
let(:reauthentication_context) { true }
@@ -146,6 +154,24 @@
)
end
end
+
+ context 'add piv cac after 2fa' do
+ let(:add_piv_cac_after_2fa) { true }
+
+ it 'returns all mfas associated with account' do
+ expect(options_classes).to eq(
+ [
+ TwoFactorAuthentication::SignInPhoneSelectionPresenter,
+ TwoFactorAuthentication::SignInPhoneSelectionPresenter,
+ TwoFactorAuthentication::SignInWebauthnSelectionPresenter,
+ TwoFactorAuthentication::SignInBackupCodeSelectionPresenter,
+ TwoFactorAuthentication::SignInPivCacSelectionPresenter,
+ TwoFactorAuthentication::SignInAuthAppSelectionPresenter,
+ TwoFactorAuthentication::SignInPersonalKeySelectionPresenter,
+ ],
+ )
+ end
+ end
end
context 'phishing resistant required' do
@@ -177,6 +203,24 @@
)
end
end
+
+ context 'add piv cac after 2fa' do
+ let(:add_piv_cac_after_2fa) { true }
+
+ it 'returns all mfas associated with account' do
+ expect(options_classes).to eq(
+ [
+ TwoFactorAuthentication::SignInPhoneSelectionPresenter,
+ TwoFactorAuthentication::SignInPhoneSelectionPresenter,
+ TwoFactorAuthentication::SignInWebauthnSelectionPresenter,
+ TwoFactorAuthentication::SignInBackupCodeSelectionPresenter,
+ TwoFactorAuthentication::SignInPivCacSelectionPresenter,
+ TwoFactorAuthentication::SignInAuthAppSelectionPresenter,
+ TwoFactorAuthentication::SignInPersonalKeySelectionPresenter,
+ ],
+ )
+ end
+ end
end
end
@@ -212,6 +256,12 @@
it { should be_nil }
end
+
+ context 'add piv cac after 2fa' do
+ let(:add_piv_cac_after_2fa) { true }
+
+ it { should be_nil }
+ end
end
context 'piv cac required' do
@@ -241,6 +291,12 @@
it { should be_nil }
end
+
+ context 'add piv cac after 2fa' do
+ let(:add_piv_cac_after_2fa) { true }
+
+ it { should be_nil }
+ end
end
end
diff --git a/spec/services/idv/steps/in_person/state_id_step_spec.rb b/spec/services/idv/steps/in_person/state_id_step_spec.rb
deleted file mode 100644
index f0056cddc65..00000000000
--- a/spec/services/idv/steps/in_person/state_id_step_spec.rb
+++ /dev/null
@@ -1,365 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe Idv::Steps::InPerson::StateIdStep do
- include InPersonHelper
- let(:submitted_values) { {} }
- let(:params) { ActionController::Parameters.new({ identity_doc: submitted_values }) }
- let(:user) { build(:user) }
- let(:formatted_dob) { InPersonHelper::GOOD_DOB }
- let(:dob) do
- parsed_dob = Date.parse(formatted_dob)
- { month: parsed_dob.month.to_s,
- day: parsed_dob.day.to_s,
- year: parsed_dob.year.to_s }
- end
- let(:enrollment) { InPersonEnrollment.new }
- let(:service_provider) { create(:service_provider) }
- let(:controller) do
- instance_double(
- 'controller',
- session: { sp: { issuer: service_provider.issuer } },
- params: params,
- current_user: user,
- url_options: {},
- )
- end
-
- let(:flow) do
- Idv::Flows::InPersonFlow.new(controller, {}, 'idv/in_person')
- end
-
- subject(:step) do
- Idv::Steps::InPerson::StateIdStep.new(flow)
- end
-
- describe '#call' do
- context 'with values submitted' do
- let(:first_name) { 'Natalya' }
- let(:last_name) { 'Rostova' }
- let(:identity_doc_address_state) { 'Nevada' }
- let(:id_number) { 'ABC123234' }
- let(:identity_doc_zipcode) { InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE }
- let(:submitted_values) do
- {
- first_name: first_name,
- last_name: last_name,
- dob: dob,
- identity_doc_address_state: identity_doc_address_state,
- id_number: id_number,
- identity_doc_zipcode: identity_doc_zipcode,
- }
- end
-
- before do
- allow(user).to receive(:establishing_in_person_enrollment).
- and_return(enrollment)
-
- Idv::StateIdForm::ATTRIBUTES.each do |attr|
- expect(flow.flow_session[:pii_from_user]).to_not have_key attr
- end
-
- @result = step.call
- end
-
- it 'sets values in flow session' do
- pii_from_user = flow.flow_session[:pii_from_user]
- expect(pii_from_user[:first_name]).to eq first_name
- expect(pii_from_user[:last_name]).to eq last_name
- expect(pii_from_user[:dob]).to eq formatted_dob
- expect(pii_from_user[:identity_doc_address_state]).to eq identity_doc_address_state
- # param from form as id_number but is renamed to state_id_number on update
- expect(pii_from_user[:state_id_number]).to eq id_number
- expect(pii_from_user[:identity_doc_zipcode]).to eq identity_doc_zipcode
- end
- end
-
- context 'when same_address_as_id is...' do
- let(:pii_from_user) { flow.flow_session[:pii_from_user] }
- let(:params) { ActionController::Parameters.new({ identity_doc: submitted_values }) }
- # residential
- let(:address1) { InPersonHelper::GOOD_ADDRESS1 }
- let(:address2) { InPersonHelper::GOOD_ADDRESS2 }
- let(:city) { InPersonHelper::GOOD_CITY }
- let(:state) { InPersonHelper::GOOD_STATE }
- let(:zipcode) { InPersonHelper::GOOD_ZIPCODE }
- # identity_doc_
- let(:identity_doc_address1) { InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1 }
- let(:identity_doc_address2) { InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2 }
- let(:identity_doc_city) { InPersonHelper::GOOD_IDENTITY_DOC_CITY }
- let(:identity_doc_address_state) { InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS_STATE }
- let(:identity_doc_zipcode) { InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE }
-
- before(:each) do
- allow(user).to receive(:establishing_in_person_enrollment).
- and_return(enrollment)
- end
-
- context 'changed from "true" to "false"' do
- let(:submitted_values) do
- {
- dob:,
- same_address_as_id: 'false', # value on submission
- address1:,
- address2:,
- city:,
- state:,
- zipcode:,
- identity_doc_address1:,
- identity_doc_address2:,
- identity_doc_city:,
- identity_doc_address_state:,
- identity_doc_zipcode:,
- }
- end
-
- before do
- Idv::StateIdForm::ATTRIBUTES.each do |attr|
- expect(flow.flow_session[:pii_from_user]).to_not have_key attr
- end
-
- make_pii
-
- # On Verify, user changes response from "Yes,..." to
- # "No, I live at a different address", see submitted_values above
- @result = step.call
- end
-
- it 'retains identity_doc_ attrs/value but removes addr attr in flow session' do
- expect(flow.flow_session[:pii_from_user]).to include(
- identity_doc_address1:,
- identity_doc_address2:,
- identity_doc_city:,
- identity_doc_address_state:,
- identity_doc_zipcode:,
- )
-
- # removes address attributes (non identity_doc_ attributes) in flow session
- expect(flow.flow_session[:pii_from_user]).not_to include(
- address1:,
- address2:,
- city:,
- state:,
- zipcode:,
- )
- end
- end
-
- context 'changed from "false" to "true"' do
- let(:submitted_values) do
- {
- dob:,
- same_address_as_id: 'true', # value on submission
- address1:, # address1 and identity_doc_address1 is innitially different
- address2:,
- city:,
- state:,
- zipcode:,
- identity_doc_address1:,
- identity_doc_address2:,
- identity_doc_city:,
- identity_doc_address_state:,
- identity_doc_zipcode:,
- }
- end
-
- before do
- Idv::StateIdForm::ATTRIBUTES.each do |attr|
- expect(flow.flow_session[:pii_from_user]).to_not have_key attr
- end
-
- make_pii(same_address_as_id: 'false')
-
- # On Verify, user changes response from "No,..." to
- # "Yes, I live at the address on my state-issued ID
- @result = step.call
- end
-
- it <<~EOS.squish do
- retains identity_doc_ attrs/value and addr attr
- with same value as identity_doc in flow session
- EOS
- expect(pii_from_user[:address1]).to eq identity_doc_address1
- expect(pii_from_user[:address2]).to eq identity_doc_address2
- expect(pii_from_user[:city]).to eq identity_doc_city
- expect(pii_from_user[:state]).to eq identity_doc_address_state
- expect(pii_from_user[:zipcode]).to eq identity_doc_zipcode
- end
- end
-
- context 'not changed from "false"' do
- let(:submitted_values) do
- {
- dob:,
- same_address_as_id: 'false',
- address1:,
- address2:,
- city:,
- state:,
- zipcode:,
- identity_doc_address1:,
- identity_doc_address2:,
- identity_doc_city:,
- identity_doc_address_state:,
- identity_doc_zipcode:,
- }
- end
- before do
- Idv::StateIdForm::ATTRIBUTES.each do |attr|
- expect(flow.flow_session[:pii_from_user]).to_not have_key attr
- end
-
- # User picks "No, I live at a different address" on state ID
- make_pii(same_address_as_id: 'false')
-
- # On Verify, user does not changes response "No,..."
- @result = step.call
- end
-
- it 'retains identity_doc_ and addr attrs/value in flow session' do
- expect(flow.flow_session[:pii_from_user]).to include(
- identity_doc_address1:,
- identity_doc_address2:,
- identity_doc_city:,
- identity_doc_address_state:,
- identity_doc_zipcode:,
- address1:,
- address2:,
- city:,
- state:,
- zipcode:,
- )
-
- # those values are different
- pii_from_user = flow.flow_session[:pii_from_user]
- expect(pii_from_user[:address1]).to_not eq identity_doc_address1
- expect(pii_from_user[:address2]).to_not eq identity_doc_address2
- expect(pii_from_user[:city]).to_not eq identity_doc_city
- expect(pii_from_user[:state]).to_not eq identity_doc_address_state
- expect(pii_from_user[:zipcode]).to_not eq identity_doc_zipcode
- end
- end
- end
-
- context 'skip address step?' do
- let(:pii_from_user) { flow.flow_session[:pii_from_user] }
- let(:params) { ActionController::Parameters.new({ identity_doc: submitted_values }) }
- let(:enrollment) { InPersonEnrollment.new }
- let(:identity_doc_address_state) { 'Nevada' }
- let(:identity_doc_city) { 'Twin Peaks' }
- let(:identity_doc_address1) { '123 Sesame Street' }
- let(:identity_doc_address2) { 'Apt. #C' }
- let(:identity_doc_zipcode) { '90001' }
- let(:submitted_values) do
- {
- dob: dob,
- identity_doc_address_state: identity_doc_address_state,
- identity_doc_city: identity_doc_city,
- identity_doc_address1: identity_doc_address1,
- identity_doc_address2: identity_doc_address2,
- identity_doc_zipcode: identity_doc_zipcode,
- same_address_as_id: same_address_as_id,
- }
- end
-
- before(:each) do
- allow(step).to receive(:current_user).
- and_return(user)
- allow(user).to receive(:establishing_in_person_enrollment).
- and_return(enrollment)
- end
-
- context 'same address as id' do
- let(:same_address_as_id) { 'true' }
-
- before do
- @result = step.call
- end
-
- it 'adds state id values to address values in pii' do
- pii_from_user = flow.flow_session[:pii_from_user]
- expect(pii_from_user[:address1]).to eq identity_doc_address1
- expect(pii_from_user[:address2]).to eq identity_doc_address2
- expect(pii_from_user[:city]).to eq identity_doc_city
- expect(pii_from_user[:state]).to eq identity_doc_address_state
- expect(pii_from_user[:zipcode]).to eq identity_doc_zipcode
- end
- end
-
- context 'different address from id' do
- let(:same_address_as_id) { 'false' }
-
- before do
- @result = step.call
- end
-
- it 'does not add state id values to address values in pii' do
- pii_from_user = flow.flow_session[:pii_from_user]
- expect(pii_from_user[:address1]).to_not eq identity_doc_address1
- expect(pii_from_user[:address2]).to_not eq identity_doc_address2
- expect(pii_from_user[:city]).to_not eq identity_doc_city
- expect(pii_from_user[:state]).to_not eq identity_doc_address_state
- expect(pii_from_user[:zipcode]).to_not eq identity_doc_zipcode
- end
- end
- end
- end
-
- describe '#extra_view_variables' do
- let(:dob) { '1972-02-23' }
- let(:first_name) { 'First name' }
- let(:pii_from_user) { flow.flow_session[:pii_from_user] }
- let(:params) { ActionController::Parameters.new }
- let(:enrollment) { InPersonEnrollment.new }
-
- before(:each) do
- allow(step).to receive(:current_user).
- and_return(user)
- allow(user).to receive(:establishing_in_person_enrollment).
- and_return(enrollment)
- end
-
- context 'first name and dob are set' do
- it 'returns extra view variables' do
- pii_from_user[:dob] = dob
- pii_from_user[:first_name] = first_name
-
- expect(step.extra_view_variables).to include(
- pii: include(
- dob: dob,
- first_name: first_name,
- ),
- parsed_dob: Date.parse(dob),
- updating_state_id: true,
- )
- end
- end
-
- context 'first name is set' do
- it 'returns extra view variables' do
- pii_from_user[:first_name] = first_name
-
- expect(step.extra_view_variables).to include(
- pii: include(
- first_name: first_name,
- ),
- parsed_dob: nil,
- updating_state_id: true,
- )
- end
- end
-
- context 'dob is set' do
- it 'returns extra view variables' do
- pii_from_user[:dob] = dob
-
- expect(step.extra_view_variables).to include(
- pii: include(
- dob: dob,
- ),
- parsed_dob: Date.parse(dob),
- updating_state_id: false,
- )
- end
- end
- end
-end
diff --git a/spec/support/features/in_person_helper.rb b/spec/support/features/in_person_helper.rb
index 396e2ac6de3..9c48b2b2b74 100644
--- a/spec/support/features/in_person_helper.rb
+++ b/spec/support/features/in_person_helper.rb
@@ -122,7 +122,7 @@ def complete_prepare_step(_user = nil)
def complete_state_id_step(_user = nil, same_address_as_id: true, first_name: GOOD_FIRST_NAME)
# Wait for page to load before attempting to fill out form
- expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10)
+ expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10)
fill_out_state_id_form_ok(same_address_as_id: same_address_as_id, first_name:)
click_idv_continue
unless same_address_as_id
@@ -163,7 +163,7 @@ def complete_steps_before_state_id_step
begin_in_person_proofing
complete_prepare_step
complete_location_step
- expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10)
+ expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10)
end
def complete_steps_before_state_id_controller
@@ -171,7 +171,6 @@ def complete_steps_before_state_id_controller
begin_in_person_proofing
complete_prepare_step
complete_location_step
-
expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10)
end
diff --git a/spec/views/idv/in_person/state_id.html.erb_spec.rb b/spec/views/idv/in_person/state_id/show.html.erb_spec.rb
similarity index 92%
rename from spec/views/idv/in_person/state_id.html.erb_spec.rb
rename to spec/views/idv/in_person/state_id/show.html.erb_spec.rb
index 658490c9423..c458adf28cf 100644
--- a/spec/views/idv/in_person/state_id.html.erb_spec.rb
+++ b/spec/views/idv/in_person/state_id/show.html.erb_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-RSpec.describe 'idv/in_person/state_id.html.erb' do
+RSpec.describe 'idv/in_person/state_id/show.html.erb' do
let(:pii) { {} }
let(:form) { Idv::StateIdForm.new(pii) }
let(:parsed_dob) { Date.new(1970, 1, 1) }
@@ -12,7 +12,7 @@
end
subject(:render_template) do
- render template: 'idv/in_person/state_id',
+ render template: 'idv/in_person/state_id/show',
locals: { updating_state_id: true, form: form, pii: pii, parsed_dob: parsed_dob }
end
diff --git a/spec/views/two_factor_authentication/options/index.html.erb_spec.rb b/spec/views/two_factor_authentication/options/index.html.erb_spec.rb
index 46e29eb14d9..97e379c4024 100644
--- a/spec/views/two_factor_authentication/options/index.html.erb_spec.rb
+++ b/spec/views/two_factor_authentication/options/index.html.erb_spec.rb
@@ -5,18 +5,22 @@
let(:phishing_resistant_required) { false }
let(:piv_cac_required) { false }
let(:reauthentication_context) { false }
+ let(:add_piv_cac_after_2fa) { false }
+
+ subject(:rendered) { render }
before do
allow(view).to receive(:user_session).and_return({})
allow(view).to receive(:current_user).and_return(User.new)
@presenter = TwoFactorLoginOptionsPresenter.new(
- user: user,
- view: view,
- reauthentication_context: reauthentication_context,
+ user:,
+ view:,
+ reauthentication_context:,
service_provider: nil,
- phishing_resistant_required: phishing_resistant_required,
- piv_cac_required: piv_cac_required,
+ phishing_resistant_required:,
+ piv_cac_required:,
+ add_piv_cac_after_2fa:,
)
@two_factor_options_form = TwoFactorLoginOptionsForm.new(user)
end
@@ -26,36 +30,34 @@
t('two_factor_authentication.login_options_title'),
)
- render
+ rendered
end
it 'has a localized heading' do
- render
-
expect(rendered).to have_content \
t('two_factor_authentication.login_options_title')
end
it 'has a localized intro text' do
- render
-
expect(rendered).to have_content \
t('two_factor_authentication.login_intro')
end
it 'has a cancel link' do
- render
-
expect(rendered).to have_link(t('links.cancel_account_creation'), href: sign_up_cancel_path)
end
+ it 'does not display info text for adding piv cac after 2fa' do
+ expect(rendered).not_to have_content(
+ t('two_factor_authentication.piv_cac_mismatch.2fa_before_add'),
+ )
+ end
+
context 'phone vendor outage' do
before do
create(:phone_configuration, user: user, phone: '(202) 555-1111')
allow_any_instance_of(OutageStatus).to receive(:vendor_outage?).and_return(false)
allow_any_instance_of(OutageStatus).to receive(:vendor_outage?).with(:sms).and_return(true)
-
- render
end
it 'renders alert banner' do
@@ -76,11 +78,20 @@
end
end
+ context 'when adding piv cac after 2fa' do
+ let(:add_piv_cac_after_2fa) { true }
+
+ it 'displays info text for adding piv cac after 2fa' do
+ expect(rendered).to have_selector(
+ '.usa-alert.usa-alert--info',
+ text: t('two_factor_authentication.piv_cac_mismatch.2fa_before_add'),
+ )
+ end
+ end
+
context 'with phishing resistant required' do
let(:phishing_resistant_required) { true }
- before { render }
-
it 'displays warning text' do
expect(rendered).to have_selector(
'.usa-alert.usa-alert--warning',
@@ -94,8 +105,6 @@
context 'with piv cac required' do
let(:piv_cac_required) { true }
- before { render }
-
it 'displays warning text' do
expect(rendered).to have_selector(
'.usa-alert.usa-alert--warning',
@@ -110,15 +119,11 @@
let(:reauthentication_context) { true }
it 'has a localized heading' do
- render
-
expect(rendered).to have_content \
t('two_factor_authentication.login_options_reauthentication_title')
end
it 'has a localized intro text' do
- render
-
expect(rendered).to have_content \
t('two_factor_authentication.login_intro_reauthentication')
end
diff --git a/spec/views/two_factor_authentication/piv_cac_mismatch/show.html.erb_spec.rb b/spec/views/two_factor_authentication/piv_cac_mismatch/show.html.erb_spec.rb
new file mode 100644
index 00000000000..d89a4e71fc8
--- /dev/null
+++ b/spec/views/two_factor_authentication/piv_cac_mismatch/show.html.erb_spec.rb
@@ -0,0 +1,56 @@
+require 'rails_helper'
+
+RSpec.describe 'two_factor_authentication/piv_cac_mismatch/show.html.erb' do
+ let(:has_other_authentication_methods) {}
+ let(:piv_cac_required) {}
+
+ subject(:rendered) { render }
+
+ before do
+ @has_other_authentication_methods = has_other_authentication_methods
+ @piv_cac_required = piv_cac_required
+ allow(view).to receive(:user_session).and_return({})
+ end
+
+ context 'when user does not have other authentication methods' do
+ let(:has_other_authentication_methods) { false }
+
+ it 'renders instructions with a link to delete their account' do
+ expect(rendered).to have_content(
+ t(
+ 'two_factor_authentication.piv_cac_mismatch.instructions_no_other_method',
+ app_name: APP_NAME,
+ ),
+ )
+ expect(rendered).to have_link(
+ t('two_factor_authentication.piv_cac_mismatch.delete_account'),
+ href: account_reset_recovery_options_url,
+ )
+ end
+ end
+
+ context 'when user has other authentication methods' do
+ let(:has_other_authentication_methods) { true }
+
+ it 'renders instructions with a link to authenticate' do
+ expect(rendered).to have_content(t('two_factor_authentication.piv_cac_mismatch.instructions'))
+ expect(rendered).to have_button(t('two_factor_authentication.piv_cac_mismatch.cta'))
+ end
+
+ context 'when piv cac is required' do
+ let(:piv_cac_required) { true }
+
+ it 'does not provide an option to skip setting up piv/cac' do
+ expect(rendered).not_to have_button(t('two_factor_authentication.piv_cac_mismatch.skip'))
+ end
+ end
+
+ context 'when piv cac is not required' do
+ let(:piv_cac_required) { false }
+
+ it 'provides an option to skip setting up piv/cac' do
+ expect(rendered).to have_button(t('two_factor_authentication.piv_cac_mismatch.skip'))
+ end
+ end
+ end
+end
diff --git a/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb b/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb
index 470609ab22d..4409370f49f 100644
--- a/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb
+++ b/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb
@@ -1,35 +1,77 @@
require 'rails_helper'
RSpec.describe 'users/piv_cac_authentication_setup/new.html.erb' do
+ let(:user) { create(:user) }
+ let(:user_session) { {} }
+ let(:in_multi_mfa_selection_flow) { false }
+
+ subject(:rendered) { render }
+
+ before do
+ allow(view).to receive(:user_session).and_return(user_session)
+ allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:in_multi_mfa_selection_flow?).and_return(in_multi_mfa_selection_flow)
+ form = OpenStruct.new
+ @presenter = PivCacAuthenticationSetupPresenter.new(user, true, form)
+ end
+
+ it 'does not show option to skip setting up piv/cac' do
+ expect(rendered).not_to have_button(t('mfa.skip'))
+ end
+
context 'user has sufficient factors' do
+ let(:user) { create(:user, :fully_registered) }
+
it 'renders a link to cancel and go back to the account page' do
- user = create(:user, :fully_registered)
- allow(view).to receive(:current_user).and_return(user)
- allow(view).to receive(:user_session).and_return(signing_up: false)
- allow(view).to receive(:in_multi_mfa_selection_flow?).and_return(false)
- form = OpenStruct.new
- @presenter = PivCacAuthenticationSetupPresenter.new(user, true, form)
+ expect(rendered).to have_link(t('links.cancel'), href: account_path)
+ end
- render
+ context 'user is in the process of setting up multiple MFAs' do
+ let(:in_multi_mfa_selection_flow) { true }
- expect(rendered).to have_link(t('links.cancel'), href: account_path)
+ it 'renders a link to choose a different option' do
+ expect(rendered).to have_link(
+ t('two_factor_authentication.choose_another_option'),
+ href: authentication_methods_setup_path,
+ )
+ end
end
end
context 'user is setting up 2FA' do
- it 'renders a link to choose a different option' do
- user = create(:user)
- allow(view).to receive(:current_user).and_return(user)
- allow(view).to receive(:user_session).and_return(signing_up: true)
- form = OpenStruct.new
- @presenter = PivCacAuthenticationSetupPresenter.new(user, false, form)
-
- render
+ let(:user) { create(:user) }
+ it 'renders a link to choose a different option' do
expect(rendered).to have_link(
t('two_factor_authentication.choose_another_option'),
href: authentication_methods_setup_path,
)
end
end
+
+ context 'when adding piv cac after 2fa' do
+ let(:user_session) { { add_piv_cac_after_2fa: true } }
+
+ it 'shows option to skip setting up piv/cac' do
+ expect(rendered).to have_button(t('mfa.skip'))
+ end
+
+ it 'renders a link to cancel and sign out' do
+ expect(rendered).to have_link(t('links.cancel'), href: sign_out_path)
+ end
+
+ context 'when SP requires PIV/CAC' do
+ before do
+ @piv_cac_required = true
+ end
+
+ it 'does not show option to skip setting up piv/cac' do
+ expect(rendered).not_to have_button(t('mfa.skip'))
+ end
+
+ it 'renders a link to cancel and sign out' do
+ expect(rendered).to have_link(t('links.cancel'), href: sign_out_path)
+ end
+ end
+ end
end
diff --git a/yarn.lock b/yarn.lock
index 6496a57e5f0..8b99b3cb96d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2459,9 +2459,9 @@ camelcase@^6.0.0:
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-lite@^1.0.30001587:
- version "1.0.30001605"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001605.tgz#ca12d7330dd8bcb784557eb9aa64f0037870d9d6"
- integrity sha512-nXwGlFWo34uliI9z3n6Qc0wZaf7zaZWA1CPZ169La5mV3I/gem7bst0vr5XQH5TJXZIMfDeZyOrZnSlVzKxxHQ==
+ version "1.0.30001669"
+ resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz"
+ integrity sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==
chai-as-promised@^7.1.1:
version "7.1.1"