diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index beb72072569..70d18d33390 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -449,7 +449,7 @@ trigger_devops:
- echo "View your applications deployment progress at https://argocd.reviewapp.identitysandbox.gov/applications/argocd/${CI_ENVIRONMENT_SLUG}?view=tree&resource="
- echo "DNS may take a while to propagate, so be patient if it doesn't show up right away"
- echo "To access the rails console, first run 'aws-vault exec sandbox-power -- aws eks update-kubeconfig --name reviewapp'"
- - echo "Then run aws-vault exec sandbox-power -- kubectl exec -it service/$CI_ENVIRONMENT_SLUG-login-chart-idp -n review-apps -- /app/bin/rails console"
+ - echo "Then run aws-vault exec sandbox-power -- kubectl exec -it service/$CI_ENVIRONMENT_SLUG-idp -n review-apps -- /app/bin/rails console"
- echo "Address of IDP review app:"
- echo https://$CI_ENVIRONMENT_SLUG.reviewapps.identitysandbox.gov
- echo "Address of PIVCAC review app:"
diff --git a/.rubocop.yml b/.rubocop.yml
index 20ec9a534fe..3b952f5e1c4 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -843,6 +843,9 @@ Rails/Blank:
Rails/CompactBlank:
Enabled: false
+Rails/DangerousColumnNames:
+ Enabled: true
+
Rails/Delegate:
Enabled: false
@@ -868,6 +871,12 @@ Rails/DynamicFindBy:
Rails/EagerEvaluationLogMessage:
Enabled: true
+Rails/EnumSyntax:
+ Enabled: true
+
+Rails/EnvLocal:
+ Enabled: true
+
Rails/ExpandedDateRange:
Enabled: true
@@ -932,6 +941,9 @@ Rails/PluckInWhere:
Rails/Present:
Enabled: false
+Rails/RedundantActiveRecordAllMethod:
+ Enabled: true
+
Rails/RedundantPresenceValidationOnBelongsTo:
Enabled: false
@@ -959,6 +971,9 @@ Rails/RootPathnameMethods:
Rails/RootPublicPath:
Enabled: true
+Rails/SelectMap:
+ Enabled: true
+
Rails/ShortI18n:
Enabled: true
@@ -998,6 +1013,9 @@ Rails/TransactionExitStatement:
Rails/UnusedIgnoredColumns:
Enabled: true
+Rails/UnusedRenderContent:
+ Enabled: true
+
Rails/WhereEquals:
Enabled: true
@@ -1013,6 +1031,9 @@ Rails/WhereNot:
Rails/WhereNotWithMultipleConditions:
Enabled: true
+Rails/WhereRange:
+ Enabled: false
+
RSpec/LeakyConstantDeclaration:
Enabled: true
diff --git a/Gemfile b/Gemfile
index dd61c0f101b..e96adfe135d 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.3'
+gem 'rails', '~> 7.1.4'
gem 'ahoy_matey', '~> 3.0'
# pod identity requires 3.188.0
@@ -120,7 +120,7 @@ group :development, :test do
gem 'rspec-rails', '~> 6.0'
gem 'rubocop', '~> 1.62.0', require: false
gem 'rubocop-performance', '~> 1.20.2', require: false
- gem 'rubocop-rails', '>= 2.5.2', require: false
+ gem 'rubocop-rails', '>= 2.26.2', require: false
gem 'rubocop-rspec', require: false
gem 'sqlite3', require: false
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 586968a65db..9dbc4929498 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -79,35 +79,35 @@ GIT
GEM
remote: https://rubygems.org/
specs:
- actioncable (7.1.3.4)
- actionpack (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ actioncable (7.1.4.1)
+ actionpack (= 7.1.4.1)
+ activesupport (= 7.1.4.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
- actionmailbox (7.1.3.4)
- actionpack (= 7.1.3.4)
- activejob (= 7.1.3.4)
- activerecord (= 7.1.3.4)
- activestorage (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ 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.3.4)
- actionpack (= 7.1.3.4)
- actionview (= 7.1.3.4)
- activejob (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ 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
rails-dom-testing (~> 2.2)
- actionpack (7.1.3.4)
- actionview (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ actionpack (7.1.4.1)
+ actionview (= 7.1.4.1)
+ activesupport (= 7.1.4.1)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
@@ -115,35 +115,35 @@ GEM
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
- actiontext (7.1.3.4)
- actionpack (= 7.1.3.4)
- activerecord (= 7.1.3.4)
- activestorage (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ 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)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (7.1.3.4)
- activesupport (= 7.1.3.4)
+ actionview (7.1.4.1)
+ activesupport (= 7.1.4.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
- activejob (7.1.3.4)
- activesupport (= 7.1.3.4)
+ activejob (7.1.4.1)
+ activesupport (= 7.1.4.1)
globalid (>= 0.3.6)
- activemodel (7.1.3.4)
- activesupport (= 7.1.3.4)
- activerecord (7.1.3.4)
- activemodel (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ 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)
timeout (>= 0.4.0)
- activestorage (7.1.3.4)
- actionpack (= 7.1.3.4)
- activejob (= 7.1.3.4)
- activerecord (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ 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)
marcel (~> 1.0)
- activesupport (7.1.3.4)
+ activesupport (7.1.4.1)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -343,7 +343,7 @@ GEM
ffi (~> 1.0)
globalid (1.2.1)
activesupport (>= 6.1)
- good_job (3.21.1)
+ good_job (3.99.1)
activejob (>= 6.0.0)
activerecord (>= 6.0.0)
concurrent-ruby (>= 1.0.2)
@@ -523,20 +523,20 @@ GEM
rackup (2.1.0)
rack (>= 3)
webrick (~> 1.8)
- rails (7.1.3.4)
- actioncable (= 7.1.3.4)
- actionmailbox (= 7.1.3.4)
- actionmailer (= 7.1.3.4)
- actionpack (= 7.1.3.4)
- actiontext (= 7.1.3.4)
- actionview (= 7.1.3.4)
- activejob (= 7.1.3.4)
- activemodel (= 7.1.3.4)
- activerecord (= 7.1.3.4)
- activestorage (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ 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)
bundler (>= 1.15.0)
- railties (= 7.1.3.4)
+ railties (= 7.1.4.1)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -551,9 +551,9 @@ GEM
rails-i18n (7.0.6)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
- railties (7.1.3.4)
- actionpack (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ railties (7.1.4.1)
+ actionpack (= 7.1.4.1)
+ activesupport (= 7.1.4.1)
irb
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -633,10 +633,11 @@ GEM
rubocop-performance (1.20.2)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0)
- rubocop-rails (2.20.2)
+ rubocop-rails (2.26.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
- rubocop (>= 1.33.0, < 2.0)
+ rubocop (>= 1.52.0, < 2.0)
+ rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (2.24.1)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
@@ -836,7 +837,7 @@ DEPENDENCIES
rack-test (>= 1.1.0)
rack-timeout
rack_session_access (>= 0.2.0)
- rails (~> 7.1.3)
+ rails (~> 7.1.4)
rails-controller-testing (>= 1.0.4)
redacted_struct
redis (>= 3.2.0)
@@ -851,7 +852,7 @@ DEPENDENCIES
rspec_junit_formatter
rubocop (~> 1.62.0)
rubocop-performance (~> 1.20.2)
- rubocop-rails (>= 2.5.2)
+ rubocop-rails (>= 2.26.2)
rubocop-rspec
ruby-progressbar
ruby-saml
diff --git a/Makefile b/Makefile
index b779ffaf0de..f1ee0b813e3 100644
--- a/Makefile
+++ b/Makefile
@@ -239,7 +239,7 @@ normalize_yaml: ## Normalizes YAML files (alphabetizes keys, fixes line length,
optimize_svg: ## Optimizes SVG images
# Exclusions:
# - `login-icon-bimi.svg` is hand-optimized and includes required metadata that would be stripped by SVGO
- find app/assets/images public -name '*.svg' -not -name 'login-icon-bimi.svg' | xargs ./node_modules/.bin/svgo
+ find app/assets/images public -name '*.svg' -not -name 'login-icon-bimi.svg' -not -name 'selfie-capture-accept-help.svg' | xargs ./node_modules/.bin/svgo
optimize_assets: optimize_svg ## Optimizes all assets
diff --git a/app/assets/images/idv/selfie-capture-accept-help.svg b/app/assets/images/idv/selfie-capture-accept-help.svg
new file mode 100644
index 00000000000..4fe255bebae
--- /dev/null
+++ b/app/assets/images/idv/selfie-capture-accept-help.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/idv/selfie-capture-help.svg b/app/assets/images/idv/selfie-capture-help.svg
new file mode 100644
index 00000000000..a90ed990f52
--- /dev/null
+++ b/app/assets/images/idv/selfie-capture-help.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/components/login_button_component.scss b/app/components/login_button_component.scss
index 7ddb4c301bd..cfc411966ce 100644
--- a/app/components/login_button_component.scss
+++ b/app/components/login_button_component.scss
@@ -9,8 +9,7 @@
@include u-text('primary-darker');
&:hover {
- @include u-bg('primary-lighter');
- @include u-text('primary-darker');
+ @include u-bg('primary-light');
}
}
@@ -22,9 +21,8 @@
&:hover {
@include u-border(1px);
- @include u-bg('white');
- @include u-text('primary-darker');
@include u-border('base');
+ @include u-bg('primary-lighter');
}
}
@@ -33,7 +31,6 @@
@include u-text('white');
&:hover {
- @include u-bg('primary-darker');
- @include u-text('white');
+ @include u-bg('primary-darkest');
}
}
diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb
index d592617a455..1108da94480 100644
--- a/app/controllers/concerns/idv/verify_info_concern.rb
+++ b/app/controllers/concerns/idv/verify_info_concern.rb
@@ -185,6 +185,7 @@ def async_state_done(current_async_state)
state: pii[:state],
state_id_jurisdiction: pii[:state_id_jurisdiction],
state_id_number: pii[:state_id_number],
+ state_id_type: pii[:state_id_type],
extra: {
address_edited: !!idv_session.address_edited,
address_line2_present: !pii[:address2].blank?,
@@ -203,9 +204,7 @@ def async_state_done(current_async_state)
},
)
- threatmetrix_reponse_body = form_response.extra.dig(
- :proofing_results, :context, :stages, :threatmetrix, :response_body
- )
+ threatmetrix_reponse_body = delete_threatmetrix_response_body(form_response)
if threatmetrix_reponse_body.present?
analytics.idv_threatmetrix_response_body(
response_body: threatmetrix_reponse_body,
@@ -275,12 +274,14 @@ def idv_result_to_form_response(
state: nil,
state_id_jurisdiction: nil,
state_id_number: nil,
+ state_id_type: nil,
extra: {}
)
state_id = result.dig(:context, :stages, :state_id)
if state_id
state_id[:state] = state if state
state_id[:state_id_jurisdiction] = state_id_jurisdiction if state_id_jurisdiction
+ state_id[:state_id_type] = state_id_type if state_id_type
if state_id_number
state_id[:state_id_number] =
StringRedacter.redact_alphanumeric(state_id_number)
@@ -314,6 +315,18 @@ def move_applicant_to_idv_session
idv_session.applicant['uuid'] = current_user.uuid
end
+ def delete_threatmetrix_response_body(form_response)
+ threatmetrix_result = form_response.extra.dig(
+ :proofing_results,
+ :context,
+ :stages,
+ :threatmetrix,
+ )
+ return if threatmetrix_result.blank?
+
+ threatmetrix_result.delete(:response_body)
+ end
+
def add_cost(token, transaction_id: nil)
Db::SpCost::AddSpCost.call(current_sp, token, transaction_id: transaction_id)
end
diff --git a/app/controllers/concerns/two_factor_authenticatable_methods.rb b/app/controllers/concerns/two_factor_authenticatable_methods.rb
index bcfee78cc87..a69ecdbfe9f 100644
--- a/app/controllers/concerns/two_factor_authenticatable_methods.rb
+++ b/app/controllers/concerns/two_factor_authenticatable_methods.rb
@@ -90,17 +90,6 @@ def check_already_authenticated
redirect_to after_sign_in_path_for(current_user)
end
- def check_sp_required_mfa_bypass(auth_method:)
- return unless service_provider_mfa_policy.user_needs_sp_auth_method_verification?
- return if service_provider_mfa_policy.phishing_resistant_required? &&
- TwoFactorAuthenticatable::AuthMethod.phishing_resistant?(auth_method)
- if service_provider_mfa_policy.piv_cac_required? &&
- auth_method == TwoFactorAuthenticatable::AuthMethod::PIV_CAC
- return
- end
- prompt_to_verify_sp_required_mfa
- end
-
def reset_attempt_count_if_user_no_longer_locked_out
return unless current_user.no_longer_locked_out?
diff --git a/app/controllers/idv/hybrid_mobile/entry_controller.rb b/app/controllers/idv/hybrid_mobile/entry_controller.rb
index fabf2f413de..5cc4f9a94b0 100644
--- a/app/controllers/idv/hybrid_mobile/entry_controller.rb
+++ b/app/controllers/idv/hybrid_mobile/entry_controller.rb
@@ -53,13 +53,6 @@ def validate_document_capture_session_id
result = Idv::DocumentCaptureSessionForm.new(document_capture_session_uuid).submit
- event_properties = result.to_h.tap do |properties|
- # See LG-8890 for context
- properties[:doc_capture_user_id?] = session[:doc_capture_user_id].present?
- end
-
- analytics.track_event 'Doc Auth', event_properties
-
if result.success?
reset_session
diff --git a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb
index 71090f7e300..a3045d5e85f 100644
--- a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb
+++ b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb
@@ -31,7 +31,7 @@ def show
document_capture_session = DocumentCaptureSession.find_by(
uuid: document_capture_session_uuid,
)
- document_capture_session.socure_docv_token = document_response.dig(
+ document_capture_session.socure_docv_transaction_token = document_response.dig(
:data,
:docvTransactionToken,
)
diff --git a/app/controllers/idv/socure/document_capture_controller.rb b/app/controllers/idv/socure/document_capture_controller.rb
index 214918d95f7..9ea30963c4c 100644
--- a/app/controllers/idv/socure/document_capture_controller.rb
+++ b/app/controllers/idv/socure/document_capture_controller.rb
@@ -41,7 +41,7 @@ def show
uuid: document_capture_session_uuid,
)
- document_capture_session.socure_docv_token = document_response.dig(
+ document_capture_session.socure_docv_transaction_token = document_response.dig(
:data,
:docvTransactionToken,
)
diff --git a/app/controllers/two_factor_authentication/backup_code_verification_controller.rb b/app/controllers/two_factor_authentication/backup_code_verification_controller.rb
index e792cc3fc86..5f3ab20849e 100644
--- a/app/controllers/two_factor_authentication/backup_code_verification_controller.rb
+++ b/app/controllers/two_factor_authentication/backup_code_verification_controller.rb
@@ -6,7 +6,6 @@ class BackupCodeVerificationController < ApplicationController
include NewDeviceConcern
prepend_before_action :authenticate_user
- before_action :check_sp_required_mfa
def show
analytics.multi_factor_auth_enter_backup_code_visit(context: context)
@@ -80,9 +79,5 @@ def backup_code_params
def handle_valid_backup_code
redirect_to after_sign_in_path_for(current_user)
end
-
- def check_sp_required_mfa
- check_sp_required_mfa_bypass(auth_method: 'backup_code')
- end
end
end
diff --git a/app/controllers/two_factor_authentication/otp_verification_controller.rb b/app/controllers/two_factor_authentication/otp_verification_controller.rb
index 57b3d5b6012..728d8620ce6 100644
--- a/app/controllers/two_factor_authentication/otp_verification_controller.rb
+++ b/app/controllers/two_factor_authentication/otp_verification_controller.rb
@@ -6,7 +6,6 @@ class OtpVerificationController < ApplicationController
include MfaSetupConcern
include NewDeviceConcern
- before_action :check_sp_required_mfa
before_action :confirm_multiple_factors_enabled
before_action :redirect_if_blank_phone, only: [:show]
before_action :confirm_voice_capability, only: [:show]
@@ -197,10 +196,6 @@ def confirmation_for_add_phone?
UserSessionContext.confirmation_context?(context) && user_fully_authenticated?
end
- def check_sp_required_mfa
- check_sp_required_mfa_bypass(auth_method: params[:otp_delivery_preference])
- end
-
def assign_phone
if updating_existing_number?
phone_changed
diff --git a/app/controllers/two_factor_authentication/totp_verification_controller.rb b/app/controllers/two_factor_authentication/totp_verification_controller.rb
index 72e0983d558..5577e37a5ed 100644
--- a/app/controllers/two_factor_authentication/totp_verification_controller.rb
+++ b/app/controllers/two_factor_authentication/totp_verification_controller.rb
@@ -5,7 +5,6 @@ class TotpVerificationController < ApplicationController
include TwoFactorAuthenticatable
include NewDeviceConcern
- before_action :check_sp_required_mfa
before_action :confirm_totp_enabled
def show
@@ -57,9 +56,5 @@ def authenticator_view_data
two_factor_authentication_method: 'authenticator',
}.merge(generic_data)
end
-
- def check_sp_required_mfa
- check_sp_required_mfa_bypass(auth_method: 'authenticator')
- end
end
end
diff --git a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb
index c58a1967315..f546783cbe4 100644
--- a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb
+++ b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb
@@ -6,7 +6,6 @@ class WebauthnVerificationController < ApplicationController
include TwoFactorAuthenticatable
include NewDeviceConcern
- before_action :check_sp_required_mfa
before_action :confirm_webauthn_enabled, only: :show
def show
@@ -124,10 +123,6 @@ def form
)
end
- def check_sp_required_mfa
- check_sp_required_mfa_bypass(auth_method: 'webauthn')
- end
-
def platform_authenticator_param?
params[:platform].to_s == 'true'
end
diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb
index 14e5fd4df50..95c4149e587 100644
--- a/app/controllers/users/sessions_controller.rb
+++ b/app/controllers/users/sessions_controller.rb
@@ -39,7 +39,7 @@ def create
return process_rate_limited if session_bad_password_count_max_exceeded?
return process_locked_out_user if current_user && user_locked_out?(current_user)
return process_rate_limited if rate_limited?
- return process_failed_captcha if !valid_captcha_result?
+ return process_failed_captcha unless valid_captcha_result? || log_captcha_failures_only?
rate_limit_password_failure = true
self.resource = warden.authenticate!(auth_options)
@@ -204,7 +204,10 @@ def handle_valid_authentication
def track_authentication_attempt
user = user_from_params || AnonymousUser.new
- success = current_user.present? && !user_locked_out?(user) && valid_captcha_result?
+ success = current_user.present? &&
+ !user_locked_out?(user) &&
+ (valid_captcha_result? || log_captcha_failures_only?)
+
analytics.email_and_password_auth(
success: success,
user_id: user.uuid,
@@ -308,6 +311,10 @@ def randomize_check_password?
SecureRandom.random_number(IdentityConfig.store.compromised_password_randomizer_value) >=
IdentityConfig.store.compromised_password_randomizer_threshold
end
+
+ def log_captcha_failures_only?
+ IdentityConfig.store.sign_in_recaptcha_log_failures_only
+ end
end
def unsafe_redirect_error(_exception)
diff --git a/app/javascript/packages/compose-components/README.md b/app/javascript/packages/compose-components/README.md
deleted file mode 100644
index fec7f76731e..00000000000
--- a/app/javascript/packages/compose-components/README.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# `@18f/identity-compose-components`
-
-A utility function to compose a set of React components and their props to a single component.
-
-Convenient for flattening a deeply-nested arrangement of context providers, for example.
-
-## Example
-
-```jsx
-const App = composeComponents(
- [FirstContext.Provider, { value: 1 }],
- [SecondContext.Provider, { value: 2 }],
- AppRoot,
-);
-
-render(App, document.getElementById('app-root'));
-```
diff --git a/app/javascript/packages/compose-components/index.js b/app/javascript/packages/compose-components/index.js
deleted file mode 100644
index 5dce9d65faf..00000000000
--- a/app/javascript/packages/compose-components/index.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { createElement } from 'react';
-
-/** @typedef {import('react').ComponentType
} ComponentType @template P */
-
-/**
- * @typedef {[ComponentType
, P]} NormalizedComponentPair
- *
- * @template P
- */
-
-/**
- * @typedef {[ComponentType
, P]|[ComponentType
]|ComponentType
} ComponentPair
- *
- * @template P
- */
-
-/**
- * A utility function to compose a set of React components and their props to a single component.
- *
- * Convenient for flattening a deeply-nested arrangement of context providers, for example.
- *
- * @example
- * ```jsx
- * const App = composeComponents(
- * [FirstContext.Provider, { value: 1 }],
- * [SecondContext.Provider, { value: 2 }],
- * AppRoot,
- * );
- *
- * render(App, document.getElementById('app-root'));
- * ```
- *
- * @param {...ComponentPair<*>} components
- *
- * @return {ComponentType<*>}
- */
-export function composeComponents(...components) {
- return function ComposedComponent() {
- /** @type {JSX.Element?} */
- let element = null;
- for (let i = components.length - 1; i >= 0; i--) {
- const componentPair = /** @type {NormalizedComponentPair<*>} */ (
- Array.isArray(components[i]) ? components[i] : [components[i]]
- );
- const [ComponentType, props] = componentPair;
- element = createElement(ComponentType, props, element);
- }
-
- return element;
- };
-}
diff --git a/app/javascript/packages/compose-components/index.spec.jsx b/app/javascript/packages/compose-components/index.spec.jsx
deleted file mode 100644
index ba0e3f9c39b..00000000000
--- a/app/javascript/packages/compose-components/index.spec.jsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { createContext, useContext } from 'react';
-import { render } from '@testing-library/react';
-import { composeComponents } from './index.js';
-
-describe('composeComponents', () => {
- it('composes components', () => {
- const FirstContext = createContext(null);
- const SecondContext = createContext(null);
- function AppRoot() {
- return (
- <>
- {useContext(FirstContext)}
- {useContext(SecondContext)}
- >
- );
- }
-
- const ComposedComponent = composeComponents(
- [FirstContext.Provider, { value: 1 }],
- [SecondContext.Provider, { value: 2 }],
- [({ children }) => <>{children}3>],
- AppRoot,
- );
-
- const { getByText } = render( );
-
- expect(getByText('123')).to.be.ok();
- });
-});
diff --git a/app/javascript/packages/compose-components/package.json b/app/javascript/packages/compose-components/package.json
deleted file mode 100644
index 29fcd9d2b79..00000000000
--- a/app/javascript/packages/compose-components/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "@18f/identity-compose-components",
- "private": true,
- "version": "1.0.0",
- "dependencies": {
- "react": "^17.0.2"
- },
- "sideEffects": false
-}
diff --git a/app/javascript/packages/document-capture/components/acuant-capture.tsx b/app/javascript/packages/document-capture/components/acuant-capture.tsx
index 8e5e0778869..e40bee7fb58 100644
--- a/app/javascript/packages/document-capture/components/acuant-capture.tsx
+++ b/app/javascript/packages/document-capture/components/acuant-capture.tsx
@@ -327,12 +327,11 @@ function AcuantCapture(
} = useContext(AcuantContext);
const { isMockClient } = useContext(UploadContext);
const { trackEvent } = useContext(AnalyticsContext);
- const { isSelfieCaptureEnabled } = useContext(SelfieCaptureContext);
+ const { isSelfieCaptureEnabled, immediatelyBeginCapture } = useContext(SelfieCaptureContext);
const fullScreenRef = useRef(null);
const inputRef = useRef(null);
const isForceUploading = useRef(false);
const isSuppressingClickLogging = useRef(false);
- const [isCapturingEnvironment, setIsCapturingEnvironment] = useState(false);
const [ownErrorMessage, setOwnErrorMessage] = useState(null);
const [hasStartedCropping, setHasStartedCropping] = useState(false);
useMemo(() => setOwnErrorMessage(null), [value]);
@@ -350,6 +349,9 @@ function AcuantCapture(
// This hook does that.
const isBackOfId = name === 'back';
useLogCameraInfo({ isBackOfId, hasStartedCropping });
+ const [isCapturingEnvironment, setIsCapturingEnvironment] = useState(
+ selfieCapture && immediatelyBeginCapture,
+ );
const {
failedCaptureAttempts,
diff --git a/app/javascript/packages/document-capture/components/acuant-selfie-camera.tsx b/app/javascript/packages/document-capture/components/acuant-selfie-camera.tsx
index 6063dce58b6..45b668081eb 100644
--- a/app/javascript/packages/document-capture/components/acuant-selfie-camera.tsx
+++ b/app/javascript/packages/document-capture/components/acuant-selfie-camera.tsx
@@ -157,12 +157,12 @@ function AcuantSelfieCamera({
CAPTURE_ALT: t('doc_auth.info.selfie_capture.action.capture'),
};
const cleanupSelfieCamera = () => {
- window.AcuantPassiveLiveness.end();
+ window.AcuantPassiveLiveness?.end();
setIsActive(false);
};
const startSelfieCamera = () => {
- window.AcuantPassiveLiveness.start(faceCaptureCallback, faceDetectionStates);
+ window.AcuantPassiveLiveness?.start(faceCaptureCallback, faceDetectionStates);
setIsActive(true);
};
diff --git a/app/javascript/packages/document-capture/components/acuant-selfie-instructions.spec.tsx b/app/javascript/packages/document-capture/components/acuant-selfie-instructions.spec.tsx
new file mode 100644
index 00000000000..c29f0378e8f
--- /dev/null
+++ b/app/javascript/packages/document-capture/components/acuant-selfie-instructions.spec.tsx
@@ -0,0 +1,29 @@
+import { render } from '@testing-library/react';
+import AcuantSelfieInstructions from './acuant-selfie-instructions';
+
+describe('SelfieInstructions', () => {
+ let getByText;
+ let queryAllByRole;
+
+ beforeEach(() => {
+ const renderedComponent = render( );
+ getByText = renderedComponent.getByText;
+ queryAllByRole = renderedComponent.queryAllByRole;
+ });
+
+ it('renders the header', () => {
+ expect(getByText('doc_auth.headings.selfie_instructions.howto')).to.exist();
+ });
+
+ it('renders the instruction graphics', () => {
+ expect(queryAllByRole('img').length).to.equal(2);
+ });
+
+ it('renders the first instruction block', () => {
+ expect(getByText('doc_auth.info.selfie_capture_help_1')).to.exist();
+ });
+
+ it('renders the second instruction block', () => {
+ expect(getByText('doc_auth.info.selfie_capture_help_2')).to.exist();
+ });
+});
diff --git a/app/javascript/packages/document-capture/components/acuant-selfie-instructions.tsx b/app/javascript/packages/document-capture/components/acuant-selfie-instructions.tsx
new file mode 100644
index 00000000000..715eb1b666e
--- /dev/null
+++ b/app/javascript/packages/document-capture/components/acuant-selfie-instructions.tsx
@@ -0,0 +1,26 @@
+import { getAssetPath } from '@18f/identity-assets';
+import { t } from '@18f/identity-i18n';
+
+export default function AcuantSelfieInstructions() {
+ return (
+ <>
+
+ {t('doc_auth.headings.selfie_instructions.howto')}
+
+
+
+
{t('doc_auth.info.selfie_capture_help_1')}
+
+
+
+
{t('doc_auth.info.selfie_capture_help_2')}
+
+ >
+ );
+}
diff --git a/app/javascript/packages/document-capture/components/document-capture-review-issues.spec.tsx b/app/javascript/packages/document-capture/components/document-capture-review-issues.spec.tsx
index cc37fdf45aa..b50b883d9b7 100644
--- a/app/javascript/packages/document-capture/components/document-capture-review-issues.spec.tsx
+++ b/app/javascript/packages/document-capture/components/document-capture-review-issues.spec.tsx
@@ -5,7 +5,6 @@ import { toFormEntryError } from '@18f/identity-document-capture/services/upload
import { I18nContext } from '@18f/identity-react-i18n';
import { I18n } from '@18f/identity-i18n';
import { expect } from 'chai';
-import { composeComponents } from '@18f/identity-compose-components';
describe('DocumentCaptureReviewIssues', () => {
const DEFAULT_OPTIONS = {
@@ -46,54 +45,51 @@ describe('DocumentCaptureReviewIssues', () => {
context('with doc error', () => {
it('renders for non doc type failure', () => {
- const props = {
- isFailedDocType: false,
- remainingSubmitAttempts: 2,
- unknownFieldErrors: [
- {
- field: 'general',
- error: toFormEntryError({ field: 'network', message: 'general error' }),
- },
- ],
- errors: [
- {
- field: 'front',
- error: toFormEntryError({ field: 'front', message: 'front side error' }),
- },
- {
- field: 'back',
- error: toFormEntryError({ field: 'back', message: 'back side error' }),
- },
- ],
- };
- const App = composeComponents(
- [
- InPersonContext.Provider,
- {
- value: {
- inPersonURL: '/verify/doc_capture',
- },
- },
- ],
- [
- I18nContext.Provider,
- {
- value: new I18n({
- strings: {
- 'idv.failure.attempts_html': 'You have %{count} attempts remaining.',
- },
- }),
- },
- ],
- [
- DocumentCaptureReviewIssues,
- {
- ...DEFAULT_OPTIONS,
- ...props,
- },
- ],
+ const { getByText, getByLabelText, getByRole, getAllByRole } = render(
+
+
+
+
+ ,
);
- const { getByText, getByLabelText, getByRole, getAllByRole } = render( );
+
const h1 = screen.getByRole('heading', { name: 'doc_auth.headings.review_issues', level: 1 });
expect(h1).to.be.ok();
diff --git a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx
index 7489c732fb1..d5cf14d439a 100644
--- a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx
+++ b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx
@@ -7,8 +7,7 @@ import type { FormStepComponentProps } from '@18f/identity-form-steps';
import GeneralError from './general-error';
import TipList from './tip-list';
import { SelfieCaptureContext } from '../context';
-import { DocumentCaptureSubheaderOne } from './documents-and-selfie-step';
-import { DocumentsCaptureStep } from './documents-step';
+import { DocumentCaptureSubheaderOne, DocumentsCaptureStep } from './documents-step';
import { SelfieCaptureStep } from './selfie-step';
import type { ReviewIssuesStepValue } from './review-issues-step';
@@ -48,9 +47,7 @@ function DocumentCaptureReviewIssues({
return (
<>
{t('doc_auth.headings.review_issues')}
- {isSelfieCaptureEnabled && (
-
- )}
+ {isSelfieCaptureEnabled && }
)}
diff --git a/app/javascript/packages/document-capture/components/document-capture.tsx b/app/javascript/packages/document-capture/components/document-capture.tsx
index 7bdbfa29934..6ffda351cab 100644
--- a/app/javascript/packages/document-capture/components/document-capture.tsx
+++ b/app/javascript/packages/document-capture/components/document-capture.tsx
@@ -7,7 +7,6 @@ import { useDidUpdateEffect } from '@18f/identity-react-hooks';
import type { FormStep } from '@18f/identity-form-steps';
import { getConfigValue } from '@18f/identity-config';
import { UploadFormEntriesError } from '../services/upload';
-import DocumentsAndSelfieStep from './documents-and-selfie-step';
import SelfieStep from './selfie-step';
import DocumentsStep from './documents-step';
import InPersonPrepareStep from './in-person-prepare-step';
@@ -39,7 +38,7 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) {
const { t } = useI18n();
const { flowPath } = useContext(UploadContext);
const { trackSubmitEvent, trackVisitEvent } = useContext(AnalyticsContext);
- const { isSelfieCaptureEnabled, docAuthSeparatePagesEnabled } = useContext(SelfieCaptureContext);
+ const { isSelfieCaptureEnabled } = useContext(SelfieCaptureContext);
const { inPersonFullAddressEntryEnabled, inPersonURL, skipDocAuth, skipDocAuthFromHandoff } =
useContext(InPersonContext);
useDidUpdateEffect(onStepChange, [stepName]);
@@ -54,11 +53,6 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) {
: InPersonLocationPostOfficeSearchStep;
// Define different states to be used in human readable array declaration
- const documentAndSelfieFormStep: FormStep = {
- name: 'documentsAndSelfie',
- form: DocumentsAndSelfieStep,
- title: t('doc_auth.headings.document_capture'),
- };
const documentFormStep: FormStep = {
name: 'documents',
form: DocumentsStep,
@@ -70,9 +64,9 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) {
title: t('doc_auth.headings.selfie_capture'),
};
const documentsFormSteps: FormStep[] =
- isSelfieCaptureEnabled && docAuthSeparatePagesEnabled && submissionError === undefined
+ isSelfieCaptureEnabled && submissionError === undefined
? [documentFormStep, selfieFormStep]
- : [documentAndSelfieFormStep];
+ : [documentFormStep];
const reviewFormStep: FormStep = {
name: 'review',
form:
diff --git a/app/javascript/packages/document-capture/components/document-side-acuant-capture.tsx b/app/javascript/packages/document-capture/components/document-side-acuant-capture.tsx
index 746e1cf5b11..0a85cea92e1 100644
--- a/app/javascript/packages/document-capture/components/document-side-acuant-capture.tsx
+++ b/app/javascript/packages/document-capture/components/document-side-acuant-capture.tsx
@@ -57,10 +57,9 @@ function DocumentSideAcuantCapture({
}: DocumentSideAcuantCaptureProps) {
const error = errors.find(({ field }) => field === side)?.error;
const { changeStepCanComplete } = useContext(FormStepsContext);
- const { isSelfieCaptureEnabled, isSelfieDesktopTestMode, docAuthSeparatePagesEnabled } =
- useContext(SelfieCaptureContext);
+ const { isSelfieCaptureEnabled, isSelfieDesktopTestMode } = useContext(SelfieCaptureContext);
const isUploadAllowed = isSelfieDesktopTestMode || !isSelfieCaptureEnabled;
- const stepCanComplete = docAuthSeparatePagesEnabled && !isReviewStep ? undefined : true;
+ const stepCanComplete = !isReviewStep ? undefined : true;
return (
-
- {isSelfieCaptureEnabled && '1. '}
- {t('doc_auth.headings.document_capture_subheader_id')}
-
- );
-}
-export default function DocumentsAndSelfieStep({
- value = {},
- onChange = () => {},
- errors = [],
- onError = () => {},
- registerField = () => undefined,
-}: FormStepComponentProps) {
- const { t } = useI18n();
- const { isMobile } = useContext(DeviceContext);
- const { isLastStep } = useContext(FormStepsContext);
- const { flowPath } = useContext(UploadContext);
- const { isSelfieCaptureEnabled } = useContext(SelfieCaptureContext);
- const pageHeaderText = isSelfieCaptureEnabled
- ? t('doc_auth.headings.document_capture_with_selfie')
- : t('doc_auth.headings.document_capture');
- const defaultSideProps: DefaultSideProps = {
- registerField,
- onChange,
- errors,
- onError,
- };
- return (
- <>
- {flowPath === 'hybrid' && }
- {pageHeaderText}
- {isSelfieCaptureEnabled && (
-
- )}
-
-
- {isSelfieCaptureEnabled && (
-
- )}
- {isLastStep ? : }
-
- >
- );
-}
diff --git a/app/javascript/packages/document-capture/components/documents-step.tsx b/app/javascript/packages/document-capture/components/documents-step.tsx
index 8fbeafbc196..cbbb0075af8 100644
--- a/app/javascript/packages/document-capture/components/documents-step.tsx
+++ b/app/javascript/packages/document-capture/components/documents-step.tsx
@@ -1,6 +1,10 @@
import { useContext } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
-import { FormStepComponentProps, FormStepsButton } from '@18f/identity-form-steps';
+import {
+ FormStepComponentProps,
+ FormStepsButton,
+ FormStepsContext,
+} from '@18f/identity-form-steps';
import { Cancel } from '@18f/identity-verify-flow';
import HybridDocCaptureWarning from './hybrid-doc-capture-warning';
import TipList from './tip-list';
@@ -51,6 +55,7 @@ export default function DocumentsStep({
registerField = () => undefined,
}: FormStepComponentProps) {
const { t } = useI18n();
+ const { isLastStep } = useContext(FormStepsContext);
const { isMobile } = useContext(DeviceContext);
const { flowPath } = useContext(UploadContext);
const defaultSideProps: DefaultSideProps = {
@@ -77,7 +82,7 @@ export default function DocumentsStep({
value={value}
isReviewStep={false}
/>
-
+ {isLastStep ? : }
>
);
diff --git a/app/javascript/packages/document-capture/components/selfie-step.tsx b/app/javascript/packages/document-capture/components/selfie-step.tsx
index 20a930e1d96..05521b021b9 100644
--- a/app/javascript/packages/document-capture/components/selfie-step.tsx
+++ b/app/javascript/packages/document-capture/components/selfie-step.tsx
@@ -1,4 +1,4 @@
-import { useContext } from 'react';
+import { useContext, useState } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import {
FormStepComponentProps,
@@ -6,6 +6,9 @@ import {
FormStepsContext,
} from '@18f/identity-form-steps';
import { Cancel } from '@18f/identity-verify-flow';
+import { SpinnerButton } from '@18f/identity-spinner-button';
+import AcuantSelfieInstructions from './acuant-selfie-instructions';
+import SelfieCaptureContext from '../context/selfie-capture';
import HybridDocCaptureWarning from './hybrid-doc-capture-warning';
import DocumentSideAcuantCapture from './document-side-acuant-capture';
import TipList from './tip-list';
@@ -20,12 +23,15 @@ export function SelfieCaptureStep({
defaultSideProps,
selfieValue,
isReviewStep,
+ showHelp,
}: {
defaultSideProps: DefaultSideProps;
selfieValue: ImageValue;
isReviewStep: boolean;
+ showHelp: boolean;
}) {
const { t } = useI18n();
+
return (
<>
{t('doc_auth.headings.document_capture_subheader_selfie')}
@@ -40,13 +46,17 @@ export function SelfieCaptureStep({
t('doc_auth.tips.document_capture_selfie_text4'),
]}
/>
-
+
+ {showHelp && }
+ {!showHelp && (
+
+ )}
>
);
}
@@ -58,8 +68,29 @@ export default function SelfieStep({
onError = () => {},
registerField = () => undefined,
}: FormStepComponentProps) {
+ const { t } = useI18n();
const { isLastStep } = useContext(FormStepsContext);
const { flowPath } = useContext(UploadContext);
+ const { showHelpInitially } = useContext(SelfieCaptureContext);
+ const [showHelp, setShowHelp] = useState(showHelpInitially);
+
+ function TakeSelfieButton() {
+ return (
+
+ {
+ setShowHelp(false);
+ }}
+ type="button"
+ isBig
+ isWide
+ >
+ {t('doc_auth.buttons.take_picture')}
+
+
+ );
+ }
const defaultSideProps: DefaultSideProps = {
registerField,
@@ -74,8 +105,11 @@ export default function SelfieStep({
defaultSideProps={defaultSideProps}
selfieValue={value.selfie}
isReviewStep={false}
+ showHelp={showHelp}
/>
- {isLastStep ? : }
+ {showHelp && }
+ {!showHelp && isLastStep && }
+ {!showHelp && !isLastStep && }
>
);
diff --git a/app/javascript/packages/document-capture/context/selfie-capture.tsx b/app/javascript/packages/document-capture/context/selfie-capture.tsx
index 7bb505658ff..6115ccd7f48 100644
--- a/app/javascript/packages/document-capture/context/selfie-capture.tsx
+++ b/app/javascript/packages/document-capture/context/selfie-capture.tsx
@@ -10,15 +10,21 @@ interface SelfieCaptureProps {
*/
isSelfieDesktopTestMode: boolean;
/**
- * Specify whether to seperate seflie upload and document upload into seperate form steps/pages
+ * Specify whether to show help and an action button before showing
+ * the capture component.
*/
- docAuthSeparatePagesEnabled: boolean;
+ showHelpInitially: boolean;
+ /**
+ * Specify whether we should try to capture using Acuant immediately
+ */
+ immediatelyBeginCapture: boolean;
}
const SelfieCaptureContext = createContext({
isSelfieCaptureEnabled: false,
isSelfieDesktopTestMode: false,
- docAuthSeparatePagesEnabled: false,
+ showHelpInitially: true,
+ immediatelyBeginCapture: false,
});
SelfieCaptureContext.displayName = 'SelfieCaptureContext';
diff --git a/app/javascript/packages/webauthn/verify-webauthn-device.spec.ts b/app/javascript/packages/webauthn/verify-webauthn-device.spec.ts
index a8cd451edf1..01a33ed2338 100644
--- a/app/javascript/packages/webauthn/verify-webauthn-device.spec.ts
+++ b/app/javascript/packages/webauthn/verify-webauthn-device.spec.ts
@@ -46,6 +46,7 @@ describe('verifyWebauthnDevice', () => {
transports: ['internal', 'hybrid'],
},
],
+ userVerification: 'discouraged',
timeout: 800000,
},
};
diff --git a/app/javascript/packages/webauthn/verify-webauthn-device.ts b/app/javascript/packages/webauthn/verify-webauthn-device.ts
index ee39c201621..8afa3092fb8 100644
--- a/app/javascript/packages/webauthn/verify-webauthn-device.ts
+++ b/app/javascript/packages/webauthn/verify-webauthn-device.ts
@@ -39,6 +39,7 @@ async function verifyWebauthnDevice({
challenge: new Uint8Array(JSON.parse(userChallenge)),
rpId: window.location.hostname,
allowCredentials: credentials.map(mapVerifyCredential),
+ userVerification: 'discouraged',
timeout: 800000,
},
})) as PublicKeyCredential;
diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx
index 9e6b6283ee2..d21223f415b 100644
--- a/app/javascript/packs/document-capture.tsx
+++ b/app/javascript/packs/document-capture.tsx
@@ -1,5 +1,4 @@
import { render } from 'react-dom';
-import { composeComponents } from '@18f/identity-compose-components';
import {
DocumentCapture,
DeviceContext,
@@ -39,10 +38,10 @@ interface AppRootData {
howToVerifyURL: string;
previousStepUrl: string;
docAuthSelfieDesktopTestMode: string;
+ accountUrl: string;
locationsUrl: string;
addressSearchUrl: string;
sessionsUrl: string;
- docAuthSeparatePagesEnabled: string;
}
const appRoot = document.getElementById('document-capture-form')!;
@@ -111,7 +110,6 @@ const {
howToVerifyUrl,
previousStepUrl,
docAuthSelfieDesktopTestMode,
- docAuthSeparatePagesEnabled,
locationsUrl: locationsURL,
addressSearchUrl: addressSearchURL,
sessionsUrl: sessionsURL,
@@ -122,94 +120,86 @@ try {
parsedUsStatesTerritories = JSON.parse(usStatesTerritories);
} catch (e) {}
-const App = composeComponents(
- [MarketingSiteContextProvider, { helpCenterRedirectURL, securityAndPrivacyHowItWorksURL }],
- [DeviceContext.Provider, { value: device }],
- [
- InPersonContext.Provider,
- {
- value: {
- inPersonURL,
- locationsURL,
- addressSearchURL,
- inPersonOutageMessageEnabled: inPersonOutageMessageEnabled === 'true',
- inPersonOutageExpectedUpdateDate,
- inPersonFullAddressEntryEnabled: inPersonFullAddressEntryEnabled === 'true',
- optedInToInPersonProofing: optedInToInPersonProofing === 'true',
- usStatesTerritories: parsedUsStatesTerritories,
- skipDocAuth: skipDocAuth === 'true',
- skipDocAuthFromHandoff: skipDocAuthFromHandoff === 'true',
- howToVerifyURL: howToVerifyUrl,
- previousStepURL: previousStepUrl,
- },
- },
- ],
- [AnalyticsContextProvider, { trackEvent }],
- [
- AcuantContextProvider,
- {
- sdkSrc: acuantVersion && `/acuant/${acuantVersion}/AcuantJavascriptWebSdk.min.js`,
- cameraSrc: acuantVersion && `/acuant/${acuantVersion}/AcuantCamera.min.js`,
- passiveLivenessOpenCVSrc: acuantVersion && `/acuant/${acuantVersion}/opencv.min.js`,
- passiveLivenessSrc: getSelfieCaptureEnabled()
- ? acuantVersion && `/acuant/${acuantVersion}/AcuantPassiveLiveness.min.js`
- : undefined,
- credentials: getMetaContent('acuant-sdk-initialization-creds'),
- endpoint: getMetaContent('acuant-sdk-initialization-endpoint'),
- glareThreshold,
- sharpnessThreshold,
- },
- ],
- [
- UploadContextProvider,
- {
- endpoint: String(appRoot.getAttribute('data-endpoint')),
- statusEndpoint: String(appRoot.getAttribute('data-status-endpoint')),
- statusPollInterval: Number(appRoot.getAttribute('data-status-poll-interval-ms')),
- isMockClient,
- formData,
- flowPath,
- },
- ],
- [
- FlowContext.Provider,
- {
- value: {
- accountURL,
- cancelURL,
- currentStep: 'document_capture',
- },
- },
- ],
- [
- ServiceProviderContextProvider,
- {
- value: getServiceProvider(),
- },
- ],
- [
- SelfieCaptureContext.Provider,
- {
- value: {
- isSelfieCaptureEnabled: getSelfieCaptureEnabled(),
- isSelfieDesktopTestMode: String(docAuthSelfieDesktopTestMode) === 'true',
- docAuthSeparatePagesEnabled: String(docAuthSeparatePagesEnabled) === 'true',
- },
- },
- ],
- [
- FailedCaptureAttemptsContextProvider,
- {
- maxCaptureAttemptsBeforeNativeCamera: Number(maxCaptureAttemptsBeforeNativeCamera),
- maxSubmissionAttemptsBeforeNativeCamera: Number(maxSubmissionAttemptsBeforeNativeCamera),
- },
- ],
- [
- DocumentCapture,
- {
- onStepChange: () => extendSession(sessionsURL),
- },
- ],
+render(
+
+
+
+
+
+
+
+
+
+
+ extendSession(sessionsURL)} />
+
+
+
+
+
+
+
+
+
+ ,
+ appRoot,
);
-
-render( , appRoot);
diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb
index 3b5296b4264..ea870cb815b 100644
--- a/app/jobs/get_usps_proofing_results_job.rb
+++ b/app/jobs/get_usps_proofing_results_job.rb
@@ -263,7 +263,7 @@ def handle_unsupported_id_type(enrollment, response)
enrollment.profile.deactivate_due_to_in_person_verification_cancelled
# send SMS and email
send_enrollment_status_sms_notification(enrollment: enrollment)
- send_failed_email(enrollment.user, enrollment)
+ send_failed_email(enrollment:, visited_location_name: response['proofingPostOffice'])
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated(
**email_analytics_attributes(enrollment),
email_type: 'Failed unsupported ID type',
@@ -305,7 +305,9 @@ def handle_expired_status_update(enrollment, response, response_message)
end
begin
- send_deadline_passed_email(enrollment.user, enrollment) unless enrollment.deadline_passed_sent
+ unless enrollment.deadline_passed_sent
+ send_deadline_passed_email(enrollment: enrollment, visited_location_name: 'none')
+ end
rescue StandardError => err
NewRelic::Agent.notice_error(err)
analytics(user: enrollment.user).
@@ -403,15 +405,16 @@ def handle_failed_status(enrollment, response)
enrollment.profile.deactivate_due_to_in_person_verification_cancelled
# send SMS and email
send_enrollment_status_sms_notification(enrollment: enrollment)
+ visited_location_name = response['proofingPostOffice']
if response['fraudSuspected']
- send_failed_fraud_email(enrollment.user, enrollment)
+ send_failed_fraud_email(enrollment:, visited_location_name:)
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated(
**email_analytics_attributes(enrollment),
email_type: 'Failed fraud suspected',
job_name: self.class.name,
)
else
- send_failed_email(enrollment.user, enrollment)
+ send_failed_email(enrollment:, visited_location_name:)
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated(
**email_analytics_attributes(enrollment),
email_type: 'Failed',
@@ -441,7 +444,7 @@ def handle_successful_status_update(enrollment, response)
# send SMS and email
send_enrollment_status_sms_notification(enrollment: enrollment)
- send_verified_email(enrollment.user, enrollment)
+ send_verified_email(enrollment:, visited_location_name: response['proofingPostOffice'])
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated(
**email_analytics_attributes(enrollment),
email_type: 'Success',
@@ -467,7 +470,7 @@ def handle_passed_with_fraud_review_pending(enrollment, response)
)
# send email
- send_please_call_email(enrollment.user, enrollment)
+ send_please_call_email(enrollment:, visited_location_name: response['proofingPostOffice'])
analytics(user: enrollment.user).
idv_in_person_usps_proofing_results_job_please_call_email_initiated(
**email_analytics_attributes(enrollment),
@@ -493,7 +496,7 @@ def handle_unsupported_secondary_id(enrollment, response)
enrollment.profile.deactivate_due_to_in_person_verification_cancelled
# send SMS and email
send_enrollment_status_sms_notification(enrollment: enrollment)
- send_failed_email(enrollment.user, enrollment)
+ send_failed_email(enrollment:, visited_location_name: response['proofingPostOffice'])
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated(
**email_analytics_attributes(enrollment),
email_type: 'Failed unsupported secondary ID',
@@ -542,51 +545,55 @@ def process_enrollment_response(enrollment, response)
end
end
- def send_verified_email(user, enrollment)
- user.confirmed_email_addresses.each do |email_address|
+ def send_verified_email(enrollment:, visited_location_name:)
+ enrollment.user.confirmed_email_addresses.each do |email_address|
# rubocop:disable IdentityIdp/MailLaterLinter
- UserMailer.with(user: user, email_address: email_address).in_person_verified(
+ UserMailer.with(user: enrollment.user, email_address: email_address).in_person_verified(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
).deliver_later(**notification_delivery_params(enrollment))
# rubocop:enable IdentityIdp/MailLaterLinter
end
end
- def send_deadline_passed_email(user, enrollment)
+ def send_deadline_passed_email(enrollment:, visited_location_name:)
# rubocop:disable IdentityIdp/MailLaterLinter
- user.confirmed_email_addresses.each do |email_address|
- UserMailer.with(user: user, email_address: email_address).in_person_deadline_passed(
- enrollment: enrollment,
- ).deliver_later
+ enrollment.user.confirmed_email_addresses.each do |email_address|
+ UserMailer.with(user: enrollment.user, email_address: email_address).
+ in_person_deadline_passed(enrollment: enrollment,
+ visited_location_name: visited_location_name).deliver_later
# rubocop:enable IdentityIdp/MailLaterLinter
end
end
- def send_failed_email(user, enrollment)
- user.confirmed_email_addresses.each do |email_address|
+ def send_failed_email(enrollment:, visited_location_name:)
+ enrollment.user.confirmed_email_addresses.each do |email_address|
# rubocop:disable IdentityIdp/MailLaterLinter
- UserMailer.with(user: user, email_address: email_address).in_person_failed(
+ UserMailer.with(user: enrollment.user, email_address: email_address).in_person_failed(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
).deliver_later(**notification_delivery_params(enrollment))
# rubocop:enable IdentityIdp/MailLaterLinter
end
end
- def send_failed_fraud_email(user, enrollment)
- user.confirmed_email_addresses.each do |email_address|
+ def send_failed_fraud_email(enrollment:, visited_location_name:)
+ enrollment.user.confirmed_email_addresses.each do |email_address|
# rubocop:disable IdentityIdp/MailLaterLinter
- UserMailer.with(user: user, email_address: email_address).in_person_failed_fraud(
+ UserMailer.with(user: enrollment.user, email_address: email_address).in_person_failed_fraud(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
).deliver_later(**notification_delivery_params(enrollment))
# rubocop:enable IdentityIdp/MailLaterLinter
end
end
- def send_please_call_email(user, enrollment)
- user.confirmed_email_addresses.each do |email_address|
+ def send_please_call_email(enrollment:, visited_location_name:)
+ enrollment.user.confirmed_email_addresses.each do |email_address|
# rubocop:disable IdentityIdp/MailLaterLinter
- UserMailer.with(user: user, email_address: email_address).in_person_please_call(
+ UserMailer.with(user: enrollment.user, email_address: email_address).in_person_please_call(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
).deliver_later(**notification_delivery_params(enrollment))
# rubocop:enable IdentityIdp/MailLaterLinter
end
diff --git a/app/jobs/good_job_v4_ready_job.rb b/app/jobs/good_job_v4_ready_job.rb
new file mode 100644
index 00000000000..0c8b9a41fd4
--- /dev/null
+++ b/app/jobs/good_job_v4_ready_job.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class GoodJobV4ReadyJob < ApplicationJob
+ queue_as :default
+
+ def perform
+ IdentityJobLogSubscriber.new.logger.info(
+ {
+ name: 'good_job_v4_ready',
+ ready: GoodJob.v4_ready?,
+ }.to_json,
+ )
+
+ true
+ end
+end
diff --git a/app/jobs/reports/protocols_report.rb b/app/jobs/reports/protocols_report.rb
index a785aeab121..ddad8d5c079 100644
--- a/app/jobs/reports/protocols_report.rb
+++ b/app/jobs/reports/protocols_report.rb
@@ -8,6 +8,11 @@ class ProtocolsReport < BaseReport
attr_accessor :report_date
+ def initialize(report_date = nil, *args, **rest)
+ @report_date = report_date
+ super(*args, **rest)
+ end
+
def perform(report_date)
return unless IdentityConfig.store.s3_reports_enabled
@@ -33,10 +38,14 @@ def perform(report_date)
private
def weekly_protocols_emailable_reports
- Reporting::ProtocolsReport.new(
+ report.as_emailable_reports
+ end
+
+ def report
+ @report ||= Reporting::ProtocolsReport.new(
issuers: nil,
time_range: report_date.all_week,
- ).as_emailable_reports
+ )
end
def report_configs
diff --git a/app/jobs/socure_reason_code_download_job.rb b/app/jobs/socure_reason_code_download_job.rb
new file mode 100644
index 00000000000..7cbf521e4cd
--- /dev/null
+++ b/app/jobs/socure_reason_code_download_job.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class SocureReasonCodeDownloadJob < ApplicationJob
+ include JobHelpers::StaleJobHelper
+
+ queue_as :low
+
+ discard_on JobHelpers::StaleJobHelper::StaleJobError
+
+ def perform
+ return unless IdentityConfig.store.idv_socure_reason_code_download_enabled
+
+ result = Proofing::Socure::ReasonCodes::Importer.new.synchronize
+ analytics.idv_socure_reason_code_download(**result.to_h)
+ end
+
+ def analytics
+ Analytics.new(
+ user: AnonymousUser.new,
+ request: nil,
+ sp: nil,
+ session: {},
+ )
+ end
+end
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 1f2e17fb19c..7fe5e30640d 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -273,12 +273,13 @@ def in_person_completion_survey
end
end
- def in_person_deadline_passed(enrollment:)
+ def in_person_deadline_passed(enrollment:, visited_location_name:)
with_user_locale(user) do
@header = t('user_mailer.in_person_deadline_passed.header')
@presenter = Idv::InPerson::VerificationResultsEmailPresenter.new(
enrollment: enrollment,
url_options: url_options,
+ visited_location_name: visited_location_name,
)
mail(
to: email_address.email,
@@ -337,12 +338,13 @@ def in_person_ready_to_verify_reminder(enrollment:)
end
end
- def in_person_verified(enrollment:)
+ def in_person_verified(enrollment:, visited_location_name:)
with_user_locale(user) do
@hide_title = true
@presenter = Idv::InPerson::VerificationResultsEmailPresenter.new(
enrollment: enrollment,
url_options: url_options,
+ visited_location_name: visited_location_name,
)
mail(
to: email_address.email,
@@ -351,11 +353,12 @@ def in_person_verified(enrollment:)
end
end
- def in_person_failed(enrollment:)
+ def in_person_failed(enrollment:, visited_location_name:)
with_user_locale(user) do
@presenter = Idv::InPerson::VerificationResultsEmailPresenter.new(
enrollment: enrollment,
url_options: url_options,
+ visited_location_name: visited_location_name,
)
mail(
to: email_address.email,
@@ -364,11 +367,12 @@ def in_person_failed(enrollment:)
end
end
- def in_person_failed_fraud(enrollment:)
+ def in_person_failed_fraud(enrollment:, visited_location_name:)
with_user_locale(user) do
@presenter = Idv::InPerson::VerificationResultsEmailPresenter.new(
enrollment: enrollment,
url_options: url_options,
+ visited_location_name: visited_location_name,
)
mail(
to: email_address.email,
@@ -377,11 +381,12 @@ def in_person_failed_fraud(enrollment:)
end
end
- def in_person_please_call(enrollment:)
+ def in_person_please_call(enrollment:, visited_location_name:)
with_user_locale(user) do
@presenter = Idv::InPerson::VerificationResultsEmailPresenter.new(
enrollment: enrollment,
url_options: url_options,
+ visited_location_name: visited_location_name,
)
@hide_title = true
mail(
diff --git a/app/models/event.rb b/app/models/event.rb
index db39c7a6657..75e33e20959 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -4,7 +4,7 @@ class Event < ApplicationRecord
belongs_to :user
belongs_to :device
- enum event_type: {
+ enum :event_type, {
account_created: 1,
phone_confirmed: 2,
password_changed: 3,
diff --git a/app/models/in_person_enrollment.rb b/app/models/in_person_enrollment.rb
index 2dd814fac4d..2490bf46db2 100644
--- a/app/models/in_person_enrollment.rb
+++ b/app/models/in_person_enrollment.rb
@@ -20,7 +20,7 @@ class InPersonEnrollment < ApplicationRecord
STATUS_EXPIRED = 'expired'
STATUS_CANCELLED = 'cancelled'
- enum status: {
+ enum :status, {
STATUS_ESTABLISHING.to_sym => 0,
STATUS_PENDING.to_sym => 1,
STATUS_PASSED.to_sym => 2,
diff --git a/app/models/phone_configuration.rb b/app/models/phone_configuration.rb
index 132a0f79b78..7320b24b951 100644
--- a/app/models/phone_configuration.rb
+++ b/app/models/phone_configuration.rb
@@ -8,7 +8,7 @@ class PhoneConfiguration < ApplicationRecord
encrypted_attribute(name: :phone)
- enum delivery_preference: { sms: 0, voice: 1 }
+ enum :delivery_preference, { sms: 0, voice: 1 }
def formatted_phone
PhoneFormatter.format(phone)
diff --git a/app/models/profile.rb b/app/models/profile.rb
index b4c6a2fd5e9..1e29f7dada1 100644
--- a/app/models/profile.rb
+++ b/app/models/profile.rb
@@ -21,7 +21,7 @@ class Profile < ApplicationRecord
class_name: 'InPersonEnrollment', foreign_key: :profile_id, inverse_of: :profile,
dependent: :destroy
- enum deactivation_reason: {
+ enum :deactivation_reason, {
password_reset: 1,
encryption_error: 2,
gpo_verification_pending_NO_LONGER_USED: 3, # deprecated
@@ -29,12 +29,12 @@ class Profile < ApplicationRecord
in_person_verification_pending_NO_LONGER_USED: 5, # deprecated
}
- enum fraud_pending_reason: {
+ enum :fraud_pending_reason, {
threatmetrix_review: 1,
threatmetrix_reject: 2,
}
- enum idv_level: {
+ enum :idv_level, {
legacy_unsupervised: 1,
legacy_in_person: 2,
unsupervised_with_selfie: 3,
diff --git a/app/models/socure_reason_code.rb b/app/models/socure_reason_code.rb
new file mode 100644
index 00000000000..1e26999eac3
--- /dev/null
+++ b/app/models/socure_reason_code.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class SocureReasonCode < ApplicationRecord
+ def self.active
+ where(deactivated_at: nil)
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 5a6accc2dbe..b5eb603eddd 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -25,7 +25,7 @@ class User < ApplicationRecord
MAX_RECENT_EVENTS = 5
MAX_RECENT_DEVICES = 5
- enum otp_delivery_preference: { sms: 0, voice: 1 }
+ enum :otp_delivery_preference, { sms: 0, voice: 1 }
# rubocop:disable Rails/HasManyOrHasOneDependent
# identities need to be orphaned to prevent UUID reuse
diff --git a/app/presenters/idv/in_person/verification_results_email_presenter.rb b/app/presenters/idv/in_person/verification_results_email_presenter.rb
index 3fae3d98a69..55a79260aa4 100644
--- a/app/presenters/idv/in_person/verification_results_email_presenter.rb
+++ b/app/presenters/idv/in_person/verification_results_email_presenter.rb
@@ -5,18 +5,15 @@ module InPerson
class VerificationResultsEmailPresenter
include Rails.application.routes.url_helpers
- attr_reader :enrollment, :url_options
+ attr_reader :enrollment, :url_options, :visited_location_name
# update to user's time zone when out of pilot
USPS_SERVER_TIMEZONE = ActiveSupport::TimeZone['America/New_York'].dup.freeze
- def initialize(enrollment:, url_options:)
+ def initialize(enrollment:, url_options:, visited_location_name:)
@enrollment = enrollment
@url_options = url_options
- end
-
- def location_name
- enrollment.selected_location_details['name']
+ @visited_location_name = visited_location_name
end
def formatted_verified_date
diff --git a/app/services/access_token_verifier.rb b/app/services/access_token_verifier.rb
index d203a25b22a..49372e308b2 100644
--- a/app/services/access_token_verifier.rb
+++ b/app/services/access_token_verifier.rb
@@ -37,7 +37,7 @@ def validate_access_token
end
def load_identity(access_token)
- identity = ServiceProviderIdentity.where(access_token: access_token).take
+ identity = ServiceProviderIdentity.find_by(access_token: access_token)
if identity && OutOfBandSessionAccessor.new(identity.rails_session_id).ttl.positive?
@identity = identity
diff --git a/app/services/agency_identity_linker.rb b/app/services/agency_identity_linker.rb
index 5a7dcc45775..0f0396649ab 100644
--- a/app/services/agency_identity_linker.rb
+++ b/app/services/agency_identity_linker.rb
@@ -16,25 +16,25 @@ def link_identity
def self.for(user:, service_provider:)
agency = service_provider.agency
- ai = AgencyIdentity.where(user: user, agency: agency).take
+ ai = AgencyIdentity.find_by(user: user, agency: agency)
return ai if ai.present?
- spi = ServiceProviderIdentity.where(
+ spi = ServiceProviderIdentity.find_by(
user: user, service_provider: service_provider.issuer,
- ).take
+ )
return nil unless spi.present?
new(spi).link_identity
end
def self.sp_identity_from_uuid_and_sp(uuid, service_provider)
- ai = AgencyIdentity.where(uuid: uuid).take
+ ai = AgencyIdentity.find_by(uuid: uuid)
criteria = if ai
{ user_id: ai.user_id, service_provider: service_provider }
else
{ uuid: uuid, service_provider: service_provider }
end
- ServiceProviderIdentity.where(criteria).take
+ ServiceProviderIdentity.find_by(criteria)
end
private
@@ -53,11 +53,11 @@ def create_agency_identity_for_sp
end
def agency_identity
- ai = AgencyIdentity.where(uuid: @sp_identity.uuid).take
+ ai = AgencyIdentity.find_by(uuid: @sp_identity.uuid)
return ai if ai
- sp = ServiceProvider.where(issuer: @sp_identity.service_provider).take
+ sp = ServiceProvider.find_by(issuer: @sp_identity.service_provider)
return unless agency_id(sp)
- AgencyIdentity.where(agency_id: agency_id, user_id: @sp_identity.user_id).take
+ AgencyIdentity.find_by(agency_id: agency_id, user_id: @sp_identity.user_id)
end
def agency_id(service_provider = nil)
diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb
index d410f516c4e..2b5f851659e 100644
--- a/app/services/analytics_events.rb
+++ b/app/services/analytics_events.rb
@@ -4622,6 +4622,31 @@ def idv_session_error_visited(
)
end
+ # Socure Reason Codes were downloaded and synced against persisted codes in the database
+ # @param [Boolean] success Result from Socure KYC API call
+ # @param [Hash] errors Result from resolution proofing
+ # @param [String] exception Exception that occured during download or synchronizaiton
+ # @param [Array] added_reason_codes New reason codes that were added to the database
+ # @param [Array] deactivated_reason_codes Old reason codes that were deactivated
+ def idv_socure_reason_code_download(
+ success: true,
+ errors: nil,
+ exception: nil,
+ added_reason_codes: nil,
+ deactivated_reason_codes: nil,
+ **extra
+ )
+ track_event(
+ :idv_socure_reason_code_download,
+ success:,
+ errors:,
+ exception:,
+ added_reason_codes:,
+ deactivated_reason_codes:,
+ **extra,
+ )
+ end
+
# Logs a Socure KYC result alongside a resolution result for later comparison.
# @param [Hash] socure_result Result from Socure KYC API call
# @param [Hash] resolution_result Result from resolution proofing
diff --git a/app/services/create_new_device_alert.rb b/app/services/create_new_device_alert.rb
index 7cda3582010..cdbb443ba63 100644
--- a/app/services/create_new_device_alert.rb
+++ b/app/services/create_new_device_alert.rb
@@ -8,7 +8,7 @@ def perform(now)
User.where(
sql_query_for_users_with_new_device,
tvalue: now - IdentityConfig.store.new_device_alert_delay_in_minutes.minutes,
- ).each do |user|
+ ).find_each do |user|
emails_sent += 1 if expire_sign_in_notification_timeframe_and_send_alert(user)
end
diff --git a/app/services/gpo_reminder_sender.rb b/app/services/gpo_reminder_sender.rb
index 741595c2773..47d49472918 100644
--- a/app/services/gpo_reminder_sender.rb
+++ b/app/services/gpo_reminder_sender.rb
@@ -8,7 +8,7 @@ def send_emails(for_letters_sent_before)
IdentityConfig.store.usps_confirmation_max_days.days.ago..for_letters_sent_before
profiles_due_for_reminder(for_letters_sent_before).each do |profile|
next if profile.user.active_profile
- profile.gpo_confirmation_codes.all.each do |gpo_code|
+ profile.gpo_confirmation_codes.find_each do |gpo_code|
next if gpo_code.reminder_sent_at
next unless reminder_eligible_range.cover?(gpo_code.created_at)
diff --git a/app/services/proofing/socure/reason_codes/api_client.rb b/app/services/proofing/socure/reason_codes/api_client.rb
new file mode 100644
index 00000000000..11c3730f748
--- /dev/null
+++ b/app/services/proofing/socure/reason_codes/api_client.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Proofing
+ module Socure
+ module ReasonCodes
+ class ApiClient
+ class ApiClientError < StandardError; end
+
+ def download_reason_codes
+ http_response = make_reason_code_http_request
+ http_response.body['reasonCodes']
+ rescue Faraday::ConnectionFailed,
+ Faraday::ServerError,
+ Faraday::SSLError,
+ Faraday::TimeoutError,
+ Faraday::ClientError,
+ Faraday::ParsingError => e
+ raise ApiClientError, e.message
+ end
+
+ private
+
+ def make_reason_code_http_request
+ conn = Faraday.new do |f|
+ f.request :instrumentation, name: 'request_metric.faraday'
+ f.response :raise_error
+ f.response :json
+ f.options.timeout = IdentityConfig.store.socure_reason_code_timeout_in_seconds
+ f.options.read_timeout = IdentityConfig.store.socure_reason_code_timeout_in_seconds
+ f.options.open_timeout = IdentityConfig.store.socure_reason_code_timeout_in_seconds
+ f.options.write_timeout = IdentityConfig.store.socure_reason_code_timeout_in_seconds
+ end
+
+ conn.get(url, { group: true }, headers) do |req|
+ req.options.context = { service_name: 'socure_reason_codes' }
+ end
+ end
+
+ def headers
+ {
+ 'Content-Type' => 'application/json',
+ 'Authorization' => "SocureApiKey #{IdentityConfig.store.socure_reason_code_api_key}",
+ }
+ end
+
+ def url
+ URI.join(
+ IdentityConfig.store.socure_reason_code_base_url,
+ '/api/3.0/reasoncodes',
+ ).to_s
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/proofing/socure/reason_codes/importer.rb b/app/services/proofing/socure/reason_codes/importer.rb
new file mode 100644
index 00000000000..4e7fbc41991
--- /dev/null
+++ b/app/services/proofing/socure/reason_codes/importer.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module Proofing
+ module Socure
+ module ReasonCodes
+ class Importer
+ class ImportError < StandardError; end
+
+ attr_reader :downloaded_reason_codes,
+ :added_reason_code_records,
+ :deactivated_reason_code_records
+
+ def initialize
+ @added_reason_code_records = []
+ @deactivated_reason_code_records = []
+ end
+
+ def synchronize
+ @downloaded_reason_codes = api_client.download_reason_codes
+
+ if !downloaded_reason_codes.is_a?(Hash) || downloaded_reason_codes.empty?
+ message = "Expected #{downloaded_reason_codes.inspect} to be a hash of reason codes"
+ raise ImportError, message
+ end
+
+ create_or_update_downloaded_reason_codes
+ deactivate_missing_reason_codes
+
+ FormResponse.new(
+ success: true,
+ errors: nil,
+ extra: {
+ added_reason_codes: format_reason_code_records(added_reason_code_records),
+ deactivated_reason_codes: format_reason_code_records(deactivated_reason_code_records),
+ },
+ )
+ rescue ApiClient::ApiClientError,
+ ImportError => e
+ FormResponse.new(
+ success: false,
+ errors: nil,
+ extra: { exception: e.inspect },
+ )
+ end
+
+ def api_client
+ @api_client ||= ApiClient.new
+ end
+
+ private
+
+ def create_or_update_downloaded_reason_codes
+ downloaded_reason_codes.each do |group, reason_codes|
+ reason_codes.each do |code, description|
+ reason_code = SocureReasonCode.find_or_initialize_by(code: code)
+ added_reason_code_records.push(reason_code) unless reason_code.persisted?
+
+ reason_code.group = group
+ reason_code.description = description
+ reason_code.added_at ||= Time.zone.now
+ reason_code.deactivated_at = nil
+ reason_code.save!
+ end
+ end
+ end
+
+ def deactivate_missing_reason_codes
+ active_codes = downloaded_reason_codes.flat_map do |_group, reason_codes|
+ reason_codes.keys
+ end
+ deactivated_at = Time.zone.now
+ SocureReasonCode.active.where.not(code: active_codes).find_each do |reason_code|
+ reason_code.update!(deactivated_at:)
+ deactivated_reason_code_records.push(reason_code)
+ end
+ end
+
+ def format_reason_code_records(socure_reason_codes)
+ socure_reason_codes.map { |r| r.attributes.slice('code', 'group', 'description') }
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/usps_in_person_proofing/enrollment_helper.rb b/app/services/usps_in_person_proofing/enrollment_helper.rb
index 859cc82dfbe..0ac3955ec3d 100644
--- a/app/services/usps_in_person_proofing/enrollment_helper.rb
+++ b/app/services/usps_in_person_proofing/enrollment_helper.rb
@@ -92,7 +92,7 @@ def cancel_stale_establishing_enrollments_for_user(user)
user.
in_person_enrollments.
where(status: :establishing).
- each(&:cancelled!)
+ find_each(&:cancelled!)
end
def usps_proofer
diff --git a/app/views/idv/in_person/ready_to_verify/show.html.erb b/app/views/idv/in_person/ready_to_verify/show.html.erb
index 41d00eeebdb..0e03425babd 100644
--- a/app/views/idv/in_person/ready_to_verify/show.html.erb
+++ b/app/views/idv/in_person/ready_to_verify/show.html.erb
@@ -232,24 +232,24 @@
<%= t('in_person_proofing.body.expect.info') %>
+ <% if @presenter.service_provider_homepage_url.blank? %>
+ <%= t('in_person_proofing.body.barcode.close_window') %>
+ <% end %>
+
+
+<%= render PageFooterComponent.new do %>
<% if @presenter.service_provider_homepage_url.present? %>
<%= render ClickObserverComponent.new(event_name: 'IdV: user clicked sp link on ready to verify page') do %>
- <%= t(
- 'in_person_proofing.body.barcode.return_to_partner_html',
- link_html: link_to(
- t(
- 'in_person_proofing.body.barcode.return_to_partner_link',
- sp_name: @presenter.sp_name,
- ),
- @presenter.service_provider_homepage_url,
+ <%= link_to(
+ t(
+ 'in_person_proofing.body.barcode.return_to_partner_link',
+ sp_name: @presenter.sp_name,
),
+ @presenter.service_provider_homepage_url,
+ class: 'display-inline-block padding-bottom-1',
) %>
<% end %>
- <% else %>
- <%= t('in_person_proofing.body.barcode.close_window') %>
+
<% end %>
-
-
-<%= render PageFooterComponent.new do %>
- <%= link_to t('in_person_proofing.body.barcode.cancel_link_text'), idv_cancel_path(step: 'barcode') %>
+ <%= link_to t('in_person_proofing.body.barcode.cancel_link_text'), idv_cancel_path(step: 'barcode'), class: 'display-inline-block padding-top-1' %>
<% end %>
diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb
index 8a92f506e7b..fc94c9754b9 100644
--- a/app/views/idv/shared/_document_capture.html.erb
+++ b/app/views/idv/shared/_document_capture.html.erb
@@ -44,7 +44,6 @@
previous_step_url: @previous_step_url,
locations_url: idv_in_person_usps_locations_url,
sessions_url: api_internal_sessions_path,
- doc_auth_separate_pages_enabled: IdentityConfig.store.doc_auth_separate_pages_enabled,
address_search_url: '',
} %>
<%= simple_form_for(
diff --git a/app/views/two_factor_authentication/options/no_option.html.erb b/app/views/two_factor_authentication/options/no_option.html.erb
deleted file mode 100644
index 3a0066adb7b..00000000000
--- a/app/views/two_factor_authentication/options/no_option.html.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<% self.title = t('titles.no_auth_option') %>
-
-<%= t('two_factor_authentication.no_auth_option') %>
diff --git a/app/views/user_mailer/in_person_deadline_passed.html.erb b/app/views/user_mailer/in_person_deadline_passed.html.erb
index 84c1af7e87d..fa65f223b93 100644
--- a/app/views/user_mailer/in_person_deadline_passed.html.erb
+++ b/app/views/user_mailer/in_person_deadline_passed.html.erb
@@ -1,11 +1,6 @@
<%= t('user_mailer.in_person_deadline_passed.body.greeting') %>
- <%= t(
- 'user_mailer.in_person_deadline_passed.body.canceled',
- app_name: APP_NAME,
- location: @presenter.location_name,
- date: @presenter.formatted_verified_date,
- ) %>
+ <%= t('user_mailer.in_person_deadline_passed.body.canceled') %>
<%= t(
diff --git a/app/views/user_mailer/in_person_failed.html.erb b/app/views/user_mailer/in_person_failed.html.erb
index 4fe54f94d4c..cc2a875aad3 100644
--- a/app/views/user_mailer/in_person_failed.html.erb
+++ b/app/views/user_mailer/in_person_failed.html.erb
@@ -2,7 +2,7 @@
<%= t(
'user_mailer.in_person_failed.intro',
- location: @presenter.location_name,
+ location: @presenter.visited_location_name,
date: @presenter.formatted_verified_date,
) %>
diff --git a/app/views/user_mailer/in_person_failed_fraud.html.erb b/app/views/user_mailer/in_person_failed_fraud.html.erb
index c92eb8e8e1e..83c61f441fb 100644
--- a/app/views/user_mailer/in_person_failed_fraud.html.erb
+++ b/app/views/user_mailer/in_person_failed_fraud.html.erb
@@ -3,7 +3,7 @@
<%= t(
'user_mailer.in_person_failed_suspected_fraud.body.intro',
app_name: APP_NAME,
- location: @presenter.location_name,
+ location: @presenter.visited_location_name,
date: @presenter.formatted_verified_date,
) %>
diff --git a/app/views/user_mailer/in_person_verified.html.erb b/app/views/user_mailer/in_person_verified.html.erb
index e1378c107f5..fdd2e7ef8e1 100644
--- a/app/views/user_mailer/in_person_verified.html.erb
+++ b/app/views/user_mailer/in_person_verified.html.erb
@@ -10,7 +10,7 @@
<%= t('user_mailer.in_person_verified.greeting') %>
<%= t(
'user_mailer.in_person_verified.intro',
- location: @presenter.location_name,
+ location: @presenter.visited_location_name,
date: @presenter.formatted_verified_date,
) %>
diff --git a/config/application.yml.default b/config/application.yml.default
index cb15308f3a6..5ef389edbf4 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -17,6 +17,8 @@
aamva_auth_request_timeout: 5.0
aamva_auth_url: 'https://example.org:12345/auth/url'
aamva_cert_enabled: true
+aamva_private_key: ''
+aamva_public_key: ''
aamva_supported_jurisdictions: '["AL","AR","AZ","CO","CT","DC","DE","FL","GA","HI","IA","ID","IL","IN","KS","KY","MA","MD","ME","MI","MO","MS","MT","NC","ND","NE","NJ","NM","NV","OH","OR","PA","RI","SC","SD","TN","TX","VA","VT","WA","WI","WV","WY"]'
aamva_verification_request_timeout: 5.0
aamva_verification_url: https://example.org:12345/verification/url
@@ -38,6 +40,8 @@ allowed_verified_within_providers: '[]'
asset_host: ''
async_stale_job_timeout_seconds: 300
async_wait_timeout_seconds: 60
+attribute_encryption_key:
+attribute_encryption_key_queue: '[]'
available_locales: 'en,es,fr,zh'
aws_http_retry_limit: 2
aws_http_retry_max_delay: 1
@@ -63,6 +67,8 @@ component_previews_enabled: false
compromised_password_randomizer_threshold: 900
compromised_password_randomizer_value: 1000
country_phone_number_overrides: '{}'
+dashboard_api_token: ''
+dashboard_url: https://dashboard.demo.login.gov
database_advisory_locks_enabled: false
database_host: ''
database_name: ''
@@ -99,7 +105,6 @@ doc_auth_max_attempts: 5
doc_auth_max_capture_attempts_before_native_camera: 3
doc_auth_max_submission_attempts_before_native_camera: 3
doc_auth_selfie_desktop_test_mode: false
-doc_auth_separate_pages_enabled: false
doc_auth_supported_country_codes: '["US", "GU", "VI", "AS", "MP", "PR", "USA" ,"GUM", "VIR", "ASM", "MNP", "PRI"]'
doc_auth_vendor: 'mock'
doc_auth_vendor_default: 'mock'
@@ -108,6 +113,7 @@ doc_auth_vendor_socure_percent: 0
doc_auth_vendor_switching_enabled: false
doc_capture_polling_enabled: true
doc_capture_request_valid_for_minutes: 15
+domain_name: login.gov
drop_off_report_config: '[{"emails":["ursula@example.com"],"issuers": ["urn:gov:gsa:openidconnect.profiles:sp:sso:agency_name:app_name"]}]'
email_from: no-reply@login.gov
email_from_display_name: Login.gov
@@ -136,6 +142,8 @@ good_job_queues: 'default:5;low:1;*'
gpo_designated_receiver_pii: '{}'
gpo_max_profile_age_to_send_letter_in_days: 30
hide_phone_mfa_signup: false
+hmac_fingerprinter_key:
+hmac_fingerprinter_key_queue: '[]'
identity_pki_disabled: false
identity_pki_local_dev: false
idv_acuant_sdk_upgrade_a_b_testing_enabled: false
@@ -149,6 +157,7 @@ idv_max_attempts: 5
idv_min_age_years: 13
idv_send_link_attempt_window_in_minutes: 10
idv_send_link_max_attempts: 5
+idv_socure_reason_code_download_enabled: false
idv_socure_shadow_mode_enabled: false
idv_sp_required: false
in_person_completion_survey_url: 'https://login.gov'
@@ -196,8 +205,12 @@ lexisnexis_phone_finder_workflow: customers.gsa2.phonefinder.workflow
lexisnexis_request_mode: testing
###################################################################
# LexisNexis DDP/ThreatMetrix #####################################
+lexisnexis_threatmetrix_api_key:
+lexisnexis_threatmetrix_base_url:
lexisnexis_threatmetrix_js_signing_cert: ''
lexisnexis_threatmetrix_mock_enabled: true
+lexisnexis_threatmetrix_org_id:
+lexisnexis_threatmetrix_policy:
lexisnexis_threatmetrix_support_code: ABCD
lexisnexis_threatmetrix_timeout: 1.0
# TrueID DocAuth Integration
@@ -219,6 +232,7 @@ login_otp_confirmation_max_attempts: 10
logins_per_email_and_ip_bantime: 60
logins_per_email_and_ip_limit: 5
logins_per_email_and_ip_period: 60
+logins_per_ip_limit: 20
logins_per_ip_period: 60
logins_per_ip_track_only_mode: false
logo_upload_enabled: false
@@ -241,6 +255,7 @@ openid_connect_content_security_form_action_enabled: false
openid_connect_redirect: client_side_js
openid_connect_redirect_issuer_override_map: '{}'
openid_connect_redirect_uuid_override_map: '{}'
+otp_delivery_blocklist_findtime: 5
otp_delivery_blocklist_maxretry: 10
otp_expiration_warning_seconds: 150
otp_min_attempts_remaining_warning_count: 3
@@ -253,6 +268,7 @@ outbound_connection_check_timeout: 5
outbound_connection_check_url: 'https://checkip.amazonaws.com'
participate_in_dap: false
password_max_attempts: 3
+password_pepper:
personal_key_retired: true
phone_carrier_registration_blocklist_array: '[]'
phone_confirmation_max_attempt_window_in_minutes: 1_440
@@ -270,6 +286,7 @@ pinpoint_voice_configs: '[]'
pinpoint_voice_pool_size: 5
piv_cac_service_timeout: 5.0
piv_cac_service_url: https://localhost:8443/
+piv_cac_verify_token_secret:
piv_cac_verify_token_url: https://localhost:8443/
poll_rate_for_verify_in_seconds: 3
prometheus_exporter: false
@@ -323,15 +340,21 @@ ruby_workers_idv_enabled: true
rules_of_use_horizon_years: 5
rules_of_use_updated_at: '2022-01-19T00:00:00Z' # Production has a newer timestamp than this, update directly in S3
s3_public_reports_enabled: false
+s3_report_bucket_prefix: login-gov.reports
+s3_report_public_bucket_prefix: login-gov-pubdata
s3_reports_enabled: false
+saml_endpoint_configs: '[]'
saml_secret_rotation_enabled: false
+scrypt_cost: 10000$8$1$
second_mfa_reminder_account_age_in_days: 30
second_mfa_reminder_sign_in_count: 10
+secret_key_base:
seed_agreements_data: true
service_provider_request_ttl_hours: 24
ses_configuration_set_name: ''
session_check_delay: 30
session_check_frequency: 30
+session_encryption_key:
session_encryptor_alert_enabled: false
session_timeout_in_minutes: 15
session_timeout_warning_seconds: 150
@@ -340,28 +363,35 @@ short_term_phone_otp_max_attempt_window_in_seconds: 10
short_term_phone_otp_max_attempts: 2
show_unsupported_passkey_platform_authentication_setup: false
show_user_attribute_deprecation_warnings: false
+sign_in_recaptcha_log_failures_only: false
sign_in_recaptcha_percent_tested: 0
sign_in_recaptcha_score_threshold: 0.0
sign_in_user_id_per_ip_attempt_window_exponential_factor: 1.1
sign_in_user_id_per_ip_attempt_window_in_minutes: 720
sign_in_user_id_per_ip_attempt_window_max_minutes: 43_200
sign_in_user_id_per_ip_max_attempts: 50
+skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:dev", "urn:gov:gsa:SAML:2.0.profiles:sp:sso:int"]'
socure_document_request_endpoint: ''
socure_enabled: false
socure_idplus_api_key: ''
socure_idplus_base_url: ''
socure_idplus_timeout_in_seconds: 5
+socure_reason_code_api_key: ''
+socure_reason_code_base_url: ''
+socure_reason_code_timeout_in_seconds: 5
socure_webhook_enabled: false
socure_webhook_secret_key: ''
socure_webhook_secret_key_queue: '[]'
sp_handoff_bounce_max_seconds: 2
sp_issuer_user_counts_report_configs: '[]'
+state_tracking_enabled: true
team_ada_email: ''
team_all_login_emails: '[]'
team_daily_fraud_metrics_emails: '[]'
team_daily_reports_emails: '[]'
team_monthly_fraud_metrics_emails: '[]'
team_ursula_email: ''
+telephony_adapter: test
test_ssn_allowed_list: ''
totp_code_interval: 30
unauthorized_scope_enabled: false
@@ -381,7 +411,11 @@ usps_ipp_transliteration_enabled: false
usps_ipp_username: ''
usps_mock_fallback: true
usps_upload_enabled: false
+usps_upload_sftp_directory: ''
+usps_upload_sftp_host: ''
+usps_upload_sftp_password: ''
usps_upload_sftp_timeout: 5
+usps_upload_sftp_username: ''
valid_authn_contexts: '["http://idmanagement.gov/ns/assurance/loa/1", "http://idmanagement.gov/ns/assurance/loa/3", "http://idmanagement.gov/ns/assurance/ial/1", "http://idmanagement.gov/ns/assurance/ial/2", "http://idmanagement.gov/ns/assurance/ial/0", "http://idmanagement.gov/ns/assurance/ial/2?strict=true", "http://idmanagement.gov/ns/assurance/ial/2?bio=preferred", "http://idmanagement.gov/ns/assurance/ial/2?bio=required", "urn:gov:gsa:ac:classes:sp:PasswordProtectedTransport:duo", "http://idmanagement.gov/ns/assurance/aal/2", "http://idmanagement.gov/ns/assurance/aal/3", "http://idmanagement.gov/ns/assurance/aal/3?hspd12=true","http://idmanagement.gov/ns/assurance/aal/2?phishing_resistant=true","http://idmanagement.gov/ns/assurance/aal/2?hspd12=true"]'
valid_authn_contexts_semantic: '["http://idmanagement.gov/ns/assurance/loa/1", "http://idmanagement.gov/ns/assurance/loa/3", "http://idmanagement.gov/ns/assurance/ial/1", "http://idmanagement.gov/ns/assurance/ial/2", "http://idmanagement.gov/ns/assurance/ial/0", "http://idmanagement.gov/ns/assurance/ial/2?strict=true", "http://idmanagement.gov/ns/assurance/ial/2?bio=preferred", "http://idmanagement.gov/ns/assurance/ial/2?bio=required", "urn:gov:gsa:ac:classes:sp:PasswordProtectedTransport:duo", "http://idmanagement.gov/ns/assurance/aal/2", "http://idmanagement.gov/ns/assurance/aal/3", "http://idmanagement.gov/ns/assurance/aal/3?hspd12=true","http://idmanagement.gov/ns/assurance/aal/2?phishing_resistant=true","http://idmanagement.gov/ns/assurance/aal/2?hspd12=true", "urn:acr.login.gov:auth-only", "urn:acr.login.gov:verified","urn:acr.login.gov:verified-facial-match-preferred","urn:acr.login.gov:verified-facial-match-required"]'
vendor_status_idv_scheduled_maintenance_finish: ''
@@ -427,7 +461,6 @@ development:
in_person_send_proofing_notifications_enabled: true
logins_per_ip_limit: 5
logo_upload_enabled: true
- otp_delivery_blocklist_findtime: 5
password_pepper: f22d4b2cafac9066fe2f4416f5b7a32c
phone_recaptcha_score_threshold: 0.5
piv_cac_verify_token_secret: ee7f20f44cdc2ba0c6830f70470d1d1d059e1279cdb58134db92b35947b1528ef5525ece5910cf4f2321ab989a618feea12ef95711dbc62b9601e8520a34ee12
@@ -439,7 +472,6 @@ development:
s3_report_bucket_prefix: ''
s3_report_public_bucket_prefix: ''
saml_endpoint_configs: '[{"suffix":"2023","secret_key_passphrase":"trust-but-verify"},{"suffix":"2024","secret_key_passphrase":"trust-but-verify"}]'
- scrypt_cost: 10000$8$1$
secret_key_base: development_secret_key_base
session_encryption_key: 27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120
show_unsupported_passkey_platform_authentication_setup: true
@@ -447,8 +479,7 @@ development:
sign_in_recaptcha_score_threshold: 0.3
skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:localhost"]'
socure_idplus_base_url: 'https://sandbox.socure.us'
- state_tracking_enabled: true
- telephony_adapter: test
+ socure_reason_code_base_url: 'https://sandbox.socure.us'
use_dashboard_service_providers: true
usps_eipp_sponsor_id: '222222222222222'
usps_ipp_sponsor_id: '111111111111111'
@@ -463,61 +494,37 @@ development:
#
production:
aamva_auth_url: 'https://authentication-cert.aamva.org/Authentication/Authenticate.svc'
- aamva_private_key: ''
- aamva_public_key: ''
aamva_verification_url: 'https://verificationservices-cert.aamva.org:18449/dldv/2.1/online'
- attribute_encryption_key:
- attribute_encryption_key_queue: '[]'
available_locales: 'en,es,fr'
biometric_ial_enabled: false
- dashboard_api_token: ''
- dashboard_url: https://dashboard.demo.login.gov
disable_email_sending: false
disable_logout_get_request: false
- domain_name: login.gov
email_registrations_per_ip_track_only_mode: true
enable_test_routes: false
enable_usps_verification: false
feature_select_email_to_share_enabled: false
feature_valid_authn_contexts_semantic_enabled: false
- hmac_fingerprinter_key:
- hmac_fingerprinter_key_queue: '[]'
idv_sp_required: true
invalid_gpo_confirmation_zipcode: ''
lexisnexis_threatmetrix_mock_enabled: false
- logins_per_ip_limit: 20
logins_per_ip_period: 20
logins_per_ip_track_only_mode: true
openid_connect_content_security_form_action_enabled: true
openid_connect_redirect: server_side
- otp_delivery_blocklist_findtime: 5
participate_in_dap: true
- password_pepper:
- piv_cac_verify_token_secret:
raise_on_component_validation_error: false
recaptcha_mock_validator: false
redis_throttle_url: redis://redis.login.gov.internal:6379/1
redis_url: redis://redis.login.gov.internal:6379
report_timeout: 1_000_000
ruby_workers_idv_enabled: false
- s3_report_bucket_prefix: login-gov.reports
- s3_report_public_bucket_prefix: login-gov-pubdata
s3_reports_enabled: true
- saml_endpoint_configs: '[]'
- scrypt_cost: 10000$8$1$
- secret_key_base:
seed_agreements_data: false
- session_encryption_key:
session_encryptor_alert_enabled: true
- skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:dev", "urn:gov:gsa:SAML:2.0.profiles:sp:sso:int"]'
state_tracking_enabled: false
telephony_adapter: pinpoint
use_kms: true
usps_auth_token_refresh_job_enabled: true
- usps_upload_sftp_directory: ''
- usps_upload_sftp_host: ''
- usps_upload_sftp_password: ''
- usps_upload_sftp_username: ''
test:
aamva_private_key: 123abc
@@ -528,7 +535,6 @@ test:
attribute_encryption_key: 2086dfbd15f5b0c584f3664422a1d3409a0d2aa6084f65b6ba57d64d4257431c124158670c7655e45cabe64194f7f7b6c7970153c285bdb8287ec0c4f7553e25
attribute_encryption_key_queue: '[{ "key": "11111111111111111111111111111111" }, { "key": "22222222222222222222222222222222" }]'
dashboard_api_token: 123ABC
- dashboard_url: https://dashboard.demo.login.gov
doc_auth_max_attempts: 4
doc_auth_selfie_desktop_test_mode: true
doc_capture_polling_enabled: false
@@ -575,13 +581,11 @@ test:
skip_encryption_allowed_list: '[]'
socure_webhook_secret_key: 'secret-key'
socure_webhook_secret_key_queue: '["old-key-one", "old-key-two"]'
- state_tracking_enabled: true
team_ada_email: 'ada@example.com'
team_all_login_emails: '["b@example.com", "c@example.com"]'
team_daily_fraud_metrics_emails: '["g@example.com", "h@example.com"]'
team_daily_reports_emails: '["a@example.com", "d@example.com"]'
team_monthly_fraud_metrics_emails: '["e@example.com", "f@example.com"]'
- telephony_adapter: test
test_ssn_allowed_list: '999999999'
totp_code_interval: 3
usps_eipp_sponsor_id: '222222222222222'
diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb
index 5559da9bb78..be5602d67ce 100644
--- a/config/initializers/job_configurations.rb
+++ b/config/initializers/job_configurations.rb
@@ -164,6 +164,11 @@
class: 'ThreatMetrixJsVerificationJob',
cron: cron_1h,
},
+ # Periodically check whether we can upgrade to GoodJob V4
+ good_job_v4_ready: {
+ class: 'GoodJobV4ReadyJob',
+ cron: cron_1h,
+ },
# Reject profiles that have been in fraud_review_pending for 30 days
fraud_rejection: {
class: 'FraudRejectionDailyJob',
@@ -242,6 +247,11 @@
cron: cron_monthly,
args: -> { [Time.zone.yesterday.end_of_day] },
},
+ # Download and store Socure reason codes
+ socure_reason_code_download: {
+ class: 'SocureReasonCodeDownloadJob',
+ cron: cron_every_monday,
+ },
}.compact
end
# rubocop:enable Metrics/BlockLength
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 471fca4dacb..ebd7685fc38 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -493,6 +493,8 @@ devise.sessions.signed_out: You are now signed out.
doc_auth.accessible_labels.camera_video_capture_instructions: We will automatically take the picture
doc_auth.accessible_labels.camera_video_capture_label: Viewfinder with frame to center your ID
doc_auth.accessible_labels.document_capture_dialog: Document capture
+doc_auth.alt.selfie_help_1: A person with their face in a green oval.
+doc_auth.alt.selfie_help_2: A finger taps a checkmark under the face to confirm the photo.
doc_auth.buttons.add_new_photos: Add new photos
doc_auth.buttons.close: Close
doc_auth.buttons.continue: Continue
@@ -578,9 +580,7 @@ doc_auth.headings.document_capture: Add photos of your driver’s license or sta
doc_auth.headings.document_capture_back: Back of your ID
doc_auth.headings.document_capture_front: Front of your ID
doc_auth.headings.document_capture_selfie: Photo of your face
-doc_auth.headings.document_capture_subheader_id: Driver’s license or state ID card
doc_auth.headings.document_capture_subheader_selfie: Add a photo of your face
-doc_auth.headings.document_capture_with_selfie: Add photos of your ID and a photo of yourself
doc_auth.headings.front: Front of your driver’s license or state ID
doc_auth.headings.how_to_verify: Choose how you want to verify your identity
doc_auth.headings.hybrid_handoff: How would you like to add your ID?
@@ -592,6 +592,7 @@ doc_auth.headings.review_issues: Check your images and try again
doc_auth.headings.secure_account: Secure your account
doc_auth.headings.selfie: Photo of your face
doc_auth.headings.selfie_capture: Capture photo of yourself
+doc_auth.headings.selfie_instructions.howto: How to take your photo
doc_auth.headings.ssn: Enter your Social Security number
doc_auth.headings.ssn_update: Update your Social Security number
doc_auth.headings.text_message: We sent a message to your phone
@@ -638,6 +639,8 @@ doc_auth.info.no_ssn: You must have a Social Security number to finish verifying
doc_auth.info.review_examples_of_photos: Review examples of how to take clear photos of your ID.
doc_auth.info.secure_account: We’ll encrypt your account when you re-enter your password. Encryption means your data is protected and only you will be able to access or change your information.
doc_auth.info.selfie_capture_content: We’ll check that you are the person on your ID.
+doc_auth.info.selfie_capture_help_1: Line up your face with the green circle. Hold still and wait for the tool to capture a photo.
+doc_auth.info.selfie_capture_help_2: After your photo is automatically captured, tap the green checkmark to accept the photo.
doc_auth.info.selfie_capture_status.face_close_to_border: Too close to the frame
doc_auth.info.selfie_capture_status.face_not_found: Face not found
doc_auth.info.selfie_capture_status.face_too_small: Face too small
@@ -1196,8 +1199,7 @@ in_person_proofing.body.barcode.location_details: Location details
in_person_proofing.body.barcode.questions: Questions?
in_person_proofing.body.barcode.retail_hours: Retail hours
in_person_proofing.body.barcode.retail_hours_closed: Closed
-in_person_proofing.body.barcode.return_to_partner_html: You may now %{link_html} to complete any next steps you can access until your identity has been verified.
-in_person_proofing.body.barcode.return_to_partner_link: sign out and return to %{sp_name}
+in_person_proofing.body.barcode.return_to_partner_link: Return to %{sp_name}
in_person_proofing.body.barcode.what_to_expect: What to expect at the Post Office
in_person_proofing.body.cta.button: Try in person
in_person_proofing.body.cta.prompt_detail: You may be able to verify your identity at a participating Post Office near you.
@@ -1595,7 +1597,6 @@ titles.idv.reset_password: Reset Password
titles.idv.verify_info: Verify your information
titles.mfa_setup.face_touch_unlock_confirmation: Face or touch unlock added
titles.mfa_setup.suggest_second_mfa: You’ve added your first authentication method! Add a second method as a backup.
-titles.no_auth_option: No sign-in method found
titles.openid_connect.authorization: OpenID Connect Authorization
titles.openid_connect.logout: OpenID Connect Logout
titles.passwords.change: Change the password for your account
@@ -1686,7 +1687,6 @@ two_factor_authentication.max_otp_requests_reached: For your security, your acco
two_factor_authentication.max_personal_key_login_attempts_reached: For your security, your account is temporarily locked because you have entered the personal key incorrectly too many times.
two_factor_authentication.max_piv_cac_login_attempts_reached: For your security, your account is temporarily locked because you have presented your piv/cac credential incorrectly too many times.
two_factor_authentication.mobile_terms_of_service: Mobile terms of service
-two_factor_authentication.no_auth_option: No authentication option could be found for you to sign in.
two_factor_authentication.opt_in.error_retry: Sorry, we are having trouble opting you in. Please try again.
two_factor_authentication.opt_in.opted_out_html: You’ve opted out of receiving text messages at %{phone_number_html}. You can opt in and receive a security code again to that phone number.
two_factor_authentication.opt_in.opted_out_last_30d_html: You’ve opted out of receiving text messages at %{phone_number_html} within the last 30 days. We can only opt in a phone number once every 30 days.
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 32d1903b196..3497461ffe1 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -504,6 +504,8 @@ devise.sessions.signed_out: Cerró su sesión.
doc_auth.accessible_labels.camera_video_capture_instructions: Tomaremos la foto automáticamente
doc_auth.accessible_labels.camera_video_capture_label: Visor con marco para centrar su identificación
doc_auth.accessible_labels.document_capture_dialog: Captura de documento
+doc_auth.alt.selfie_help_1: Una persona con el rostro dentro de un óvalo verde.
+doc_auth.alt.selfie_help_2: Un dedo toca una marca de verificación debajo de la cara para confirmar la foto.
doc_auth.buttons.add_new_photos: Añadir nuevas fotos
doc_auth.buttons.close: Cerrar
doc_auth.buttons.continue: Continuar
@@ -589,9 +591,7 @@ doc_auth.headings.document_capture: Añade fotos de tu licencia de conducir o cr
doc_auth.headings.document_capture_back: Reverso de su identificación
doc_auth.headings.document_capture_front: Frente de su identificación
doc_auth.headings.document_capture_selfie: Fotografía de su cara
-doc_auth.headings.document_capture_subheader_id: Licencia de conducir o identificación estatal
doc_auth.headings.document_capture_subheader_selfie: Añada una foto de su cara
-doc_auth.headings.document_capture_with_selfie: Agregue fotos de su identificación y una foto de usted
doc_auth.headings.front: Frente de su licencia de conducir o identificación estatal
doc_auth.headings.how_to_verify: Elija cómo desea verificar su identidad
doc_auth.headings.hybrid_handoff: '¿Cómo desea añadir su identificación?'
@@ -603,6 +603,7 @@ doc_auth.headings.review_issues: Revise sus imágenes e inténtelo de nuevo
doc_auth.headings.secure_account: Proteja su cuenta
doc_auth.headings.selfie: Fotografía de su cara
doc_auth.headings.selfie_capture: Capture una foto de usted
+doc_auth.headings.selfie_instructions.howto: 'Cómo tomar su fotografía:'
doc_auth.headings.ssn: Ingrese su número de Seguro Social
doc_auth.headings.ssn_update: Actualice su número de Seguro Social
doc_auth.headings.text_message: Enviamos un mensaje a su teléfono
@@ -649,6 +650,8 @@ doc_auth.info.no_ssn: Debe tener un número de Seguro Social para finalizar la v
doc_auth.info.review_examples_of_photos: Vea ejemplos de cómo tomar fotos nítidas de su identificación.
doc_auth.info.secure_account: Cifraremos su cuenta cuando vuelva a ingresar su contraseña. Con el cifrado, sus datos están protegidos y solo usted puede acceder a su información o modificarla.
doc_auth.info.selfie_capture_content: Revisaremos que usted sea la persona que figura en su identificación.
+doc_auth.info.selfie_capture_help_1: Alinee su cara con el círculo verde. No se mueva y espere a que la cámara capture una foto.
+doc_auth.info.selfie_capture_help_2: Después de la captura automática de su foto, toque la marca de verificación verde para aceptar la fotografía.
doc_auth.info.selfie_capture_status.face_close_to_border: Demasiado cerca del marco
doc_auth.info.selfie_capture_status.face_not_found: No se detectó el rostro
doc_auth.info.selfie_capture_status.face_too_small: Rostro demasiado pequeño
@@ -1207,8 +1210,7 @@ in_person_proofing.body.barcode.location_details: Detalles del lugar
in_person_proofing.body.barcode.questions: '¿Tiene alguna pregunta?'
in_person_proofing.body.barcode.retail_hours: Horario de atención al público
in_person_proofing.body.barcode.retail_hours_closed: Cerrado
-in_person_proofing.body.barcode.return_to_partner_html: Ahora puede %{link_html} para completar los pasos siguientes a los que tenga acceso hasta que se haya verificado su identidad.
-in_person_proofing.body.barcode.return_to_partner_link: cerrar sesión y volver a %{sp_name}
+in_person_proofing.body.barcode.return_to_partner_link: Volver a %{sp_name}
in_person_proofing.body.barcode.what_to_expect: Qué esperar en la oficina de correos
in_person_proofing.body.cta.button: Intentar en persona
in_person_proofing.body.cta.prompt_detail: Es posible que pueda verificar su identidad en una oficina de correos participante cercana.
@@ -1607,7 +1609,6 @@ titles.idv.reset_password: Restablecer la contraseña
titles.idv.verify_info: Verifique su información
titles.mfa_setup.face_touch_unlock_confirmation: Se agregó el desbloqueo facial o táctil
titles.mfa_setup.suggest_second_mfa: Agregó su primer método de autenticación. Agregue un segundo método como respaldo.
-titles.no_auth_option: No se encontró ningún método de inicio de sesión
titles.openid_connect.authorization: Autorización de OpenID Connect
titles.openid_connect.logout: Cierre de sesión de OpenID Connect
titles.passwords.change: Cambie la contraseña de su cuenta
@@ -1698,7 +1699,6 @@ two_factor_authentication.max_otp_requests_reached: Para su seguridad, su cuenta
two_factor_authentication.max_personal_key_login_attempts_reached: Para su seguridad, su cuenta está bloqueada temporalmente porque ingresó mal la clave personal demasiadas veces.
two_factor_authentication.max_piv_cac_login_attempts_reached: Para su seguridad, su cuenta está bloqueada temporalmente porque presentó mal su tarjeta PIV o CAC demasiadas veces.
two_factor_authentication.mobile_terms_of_service: Condiciones del servicio móvil
-two_factor_authentication.no_auth_option: No se pudo encontrar ninguna opción de autenticación para que iniciara sesión.
two_factor_authentication.opt_in.error_retry: Lo sentimos, estamos teniendo problema para admitirlo; inténtelo de nuevo.
two_factor_authentication.opt_in.opted_out_html: Optó por no recibir mensajes de texto en el %{phone_number_html}. Puede optar por recibir un código de seguridad de nuevo en ese número de teléfono.
two_factor_authentication.opt_in.opted_out_last_30d_html: Optó por no recibir mensajes de texto en el %{phone_number_html} en los últimos 30 días. Solo podemos admitir un número telefónico una vez cada 30 días.
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index f489171a2f5..e49b209a072 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -493,6 +493,8 @@ devise.sessions.signed_out: Vous êtes maintenant déconnecté.
doc_auth.accessible_labels.camera_video_capture_instructions: Nous prendrons automatiquement la photo
doc_auth.accessible_labels.camera_video_capture_label: Viseur avec cadre pour centrer votre pièce d’identité
doc_auth.accessible_labels.document_capture_dialog: Capture du document
+doc_auth.alt.selfie_help_1: Le visage d’une personne dans un ovale vert.
+doc_auth.alt.selfie_help_2: Un doigt coche une case sous le visage pour valider la photo.
doc_auth.buttons.add_new_photos: Ajouter de nouvelles photos
doc_auth.buttons.close: Fermer
doc_auth.buttons.continue: Suite
@@ -578,9 +580,7 @@ doc_auth.headings.document_capture: Ajoutez des photos de votre permis de condui
doc_auth.headings.document_capture_back: Verso de votre pièce d’identité
doc_auth.headings.document_capture_front: Recto de votre carte d’identité
doc_auth.headings.document_capture_selfie: Photo de votre visage
-doc_auth.headings.document_capture_subheader_id: Permis de conduire ou carte d’identité d’un État
doc_auth.headings.document_capture_subheader_selfie: Ajouter une photo de votre visage
-doc_auth.headings.document_capture_with_selfie: Ajouter des photos de votre pièce d’identité et une photo de vous-même
doc_auth.headings.front: Recto de votre permis de conduire ou de votre carte d’identité d’un État
doc_auth.headings.how_to_verify: Choisir la manière dont vous souhaitez confirmer votre identité
doc_auth.headings.hybrid_handoff: Comment voulez-vous ajouter votre pièce d’identité ?
@@ -592,6 +592,7 @@ doc_auth.headings.review_issues: Vérifiez vos images et essayez à nouveau
doc_auth.headings.secure_account: Sécuriser votre compte
doc_auth.headings.selfie: Photo de votre visage
doc_auth.headings.selfie_capture: Prenez-vous en photo
+doc_auth.headings.selfie_instructions.howto: 'Comment vous prendre en photo :'
doc_auth.headings.ssn: Saisir votre numéro de sécurité sociale
doc_auth.headings.ssn_update: Mettre à jour votre numéro de sécurité sociale
doc_auth.headings.text_message: Nous avons envoyé un message à votre téléphone
@@ -638,6 +639,8 @@ doc_auth.info.no_ssn: Vous devez avoir un numéro de sécurité sociale pour ter
doc_auth.info.review_examples_of_photos: Voir des exemples qui montrent comment prendre des photos nettes de votre pièce d’identité.
doc_auth.info.secure_account: Nous chiffrons votre compte lorsque vous resaisissez votre mot de passe. Le chiffrement signifie que vos données sont protégées et que vous êtes le seul à pouvoir accéder à vos informations ou les modifier.
doc_auth.info.selfie_capture_content: Nous vérifierons que vous êtes la personne figurant sur la pièce d’identité.
+doc_auth.info.selfie_capture_help_1: Placez votre visage à l’intérieur du cercle vert. Ne bougez pas et attendez que l’appareil se déclenche.
+doc_auth.info.selfie_capture_help_2: Une fois la photo prise automatiquement, cochez la case sous la photo pour l’accepter (la case deviendra verte).
doc_auth.info.selfie_capture_status.face_close_to_border: Trop près du cadre
doc_auth.info.selfie_capture_status.face_not_found: Visage non trouvé
doc_auth.info.selfie_capture_status.face_too_small: Visage trop petit
@@ -1196,8 +1199,7 @@ in_person_proofing.body.barcode.location_details: Détails de l’emplacement
in_person_proofing.body.barcode.questions: Des questions ?
in_person_proofing.body.barcode.retail_hours: Heures d’ouverture
in_person_proofing.body.barcode.retail_hours_closed: Fermé
-in_person_proofing.body.barcode.return_to_partner_html: Vous pouvez maintenant %{link_html} afin d’effectuer toutes les étapes suivantes auxquelles vous pouvez accéder jusqu’à ce que votre identité ait été vérifiée.
-in_person_proofing.body.barcode.return_to_partner_link: vous déconnecter et retourner à %{sp_name}
+in_person_proofing.body.barcode.return_to_partner_link: Retourner à %{sp_name}
in_person_proofing.body.barcode.what_to_expect: À quoi s’attendre au bureau de poste
in_person_proofing.body.cta.button: Essayer en personne
in_person_proofing.body.cta.prompt_detail: Vous pourrez peut-être confirmer votre identité dans un bureau de poste participant près de chez vous.
@@ -1595,7 +1597,6 @@ titles.idv.reset_password: Réinitialiser le mot de passe
titles.idv.verify_info: Vérifier vos informations
titles.mfa_setup.face_touch_unlock_confirmation: Déverrouillage facial ou tactile ajouté
titles.mfa_setup.suggest_second_mfa: Vous avez ajouté votre première méthode d’authentification ! Ajoutez-en une deuxième en guise de sauvegarde.
-titles.no_auth_option: Aucune méthode de connexion trouvée
titles.openid_connect.authorization: Autorisation OpenID Connect
titles.openid_connect.logout: Déconnexion OpenID Connect
titles.passwords.change: Changer le mot de passe de votre compte
@@ -1686,7 +1687,6 @@ two_factor_authentication.max_otp_requests_reached: Pour votre sécurité, votre
two_factor_authentication.max_personal_key_login_attempts_reached: Pour votre sécurité, votre compte est temporairement verrouillé en raison de la saisie erronée de la clé personnelle à de trop nombreuses reprises.
two_factor_authentication.max_piv_cac_login_attempts_reached: Pour votre sécurité, votre compte est temporairement verrouillé en raison de la présentation erronée de votre certificat PIV/CAC à de trop nombreuses reprises.
two_factor_authentication.mobile_terms_of_service: Conditions de service mobile
-two_factor_authentication.no_auth_option: Aucune option d’authentification n’a pu être trouvée pour vous connecter
two_factor_authentication.opt_in.error_retry: Désolé, nous rencontrons actuellement des difficultés pour vous inscrire. Veuillez réessayer.
two_factor_authentication.opt_in.opted_out_html: Vous avez choisi de ne pas recevoir de SMS au %{phone_number_html}. Vous pouvez vous inscrire et recevoir à nouveau un code de sécurité à ce numéro de téléphone.
two_factor_authentication.opt_in.opted_out_last_30d_html: Vous avez choisi de ne plus recevoir de SMS au %{phone_number_html} au cours des 30 derniers jours. Nous ne pouvons inscrire un numéro de téléphone qu’une fois tous les 30 jours.
diff --git a/config/locales/zh.yml b/config/locales/zh.yml
index 0e2b88a7b64..bcc3a9451e0 100644
--- a/config/locales/zh.yml
+++ b/config/locales/zh.yml
@@ -16,7 +16,7 @@ account_reset.delete_account.info: 被锁在账户之外时,取消账户应当
account_reset.delete_account.title: 删除账户应当是你最后的选择。
account_reset.pending.cancel_request: 取消请求
account_reset.pending.canceled: 我们已取消了你删除帐户的请求。
-account_reset.pending.confirm: 如果现在取消,你如果要删除账户的话,必须提出新请求并再等待%{interval} 。
+account_reset.pending.confirm: 如果现在取消,你如果要删除账户的话,必须提出新请求并再等待%{interval}。
account_reset.pending.header: 你已提出删除账户的请求。
account_reset.pending.wait_html: 要删除你的账户,有一个%{waiting_period}的等待期。你会在 %{interval} 收到电邮,向你说明如何完成删除。
account_reset.recovery_options.check_saved_credential: 查看你是否有已保存的凭据
@@ -504,6 +504,8 @@ devise.sessions.signed_out: 你现在已登出。
doc_auth.accessible_labels.camera_video_capture_instructions: 我们会自动拍张照片。
doc_auth.accessible_labels.camera_video_capture_label: 带框的取景器能把你身份证件放在中间。
doc_auth.accessible_labels.document_capture_dialog: 文档扫描
+doc_auth.alt.selfie_help_1: 人脸在绿色椭圆中。
+doc_auth.alt.selfie_help_2: 手指点击面孔下的勾选标记来确认照片。
doc_auth.buttons.add_new_photos: 添加新照片
doc_auth.buttons.close: 关闭
doc_auth.buttons.continue: 继续
@@ -577,7 +579,7 @@ doc_auth.errors.sharpness.top_msg_plural: 我们无法读取你的身份证件
doc_auth.errors.upload_error: 抱歉,我们这边出错了。
doc_auth.forms.captured_image: 扫描到的图像
doc_auth.forms.change_file: 更改文件
-doc_auth.forms.choose_file_html: 将文件拖到此处或者 从文件夹中选择 。
+doc_auth.forms.choose_file_html: 将文件拖到此处或者从文件夹中选择 。
doc_auth.forms.doc_success: 我们验证了你的信息
doc_auth.forms.selected_file: 被选文件
doc_auth.headings.address: 更新你的邮政地址
@@ -589,9 +591,7 @@ doc_auth.headings.document_capture: 添加你身份证件的照片
doc_auth.headings.document_capture_back: 你身份证件的背面
doc_auth.headings.document_capture_front: 你身份证件的正面
doc_auth.headings.document_capture_selfie: 你面部照片
-doc_auth.headings.document_capture_subheader_id: 驾照或州政府颁发的身份证件
doc_auth.headings.document_capture_subheader_selfie: 添加您面部照片
-doc_auth.headings.document_capture_with_selfie: 添加你身份证件和本人的照片
doc_auth.headings.front: 驾照或州政府颁发身份证件的正面
doc_auth.headings.how_to_verify: 选择你想如何验证身份
doc_auth.headings.hybrid_handoff: 你想怎么添加身份证件?
@@ -603,6 +603,7 @@ doc_auth.headings.review_issues: 检查一下你的图片并再试一次
doc_auth.headings.secure_account: 保护你的账户安全
doc_auth.headings.selfie: 照片
doc_auth.headings.selfie_capture: 拍你本人照片
+doc_auth.headings.selfie_instructions.howto: '如何拍摄你本人的照片:'
doc_auth.headings.ssn: 输入你的社会保障号码
doc_auth.headings.ssn_update: 更新你的社会保障号码
doc_auth.headings.text_message: 我们给你的手机发了短信
@@ -649,6 +650,8 @@ doc_auth.info.no_ssn: 你必须有社会保障号码才能完成身份验证。
doc_auth.info.review_examples_of_photos: 查看如何拍出身份证件清晰照片的示例。
doc_auth.info.secure_account: 我们会用你的密码对你的账户加密。加密意味着你的数据得到了保护,而且只有你能够访问或变更你的信息。
doc_auth.info.selfie_capture_content: 我们会查看你是身份证件上的人。
+doc_auth.info.selfie_capture_help_1: 把你的面部放在绿色圆圈里。请保持静止,等待工具拍照。
+doc_auth.info.selfie_capture_help_2: 你的照片被自动拍后,点击绿色勾选标志来接受照片。
doc_auth.info.selfie_capture_status.face_close_to_border: 距离相框太近
doc_auth.info.selfie_capture_status.face_not_found: 没找到面孔
doc_auth.info.selfie_capture_status.face_too_small: 面孔太小
@@ -1056,7 +1059,7 @@ idv.failure.phone.rate_limited.option_verify_by_mail_html: 通过普通邮件验
idv.failure.phone.rate_limited.options_header: 你可以:
idv.failure.phone.timeout: 我们请你验证自己信息的请求已过期。请再试一次。
idv.failure.phone.warning.attempts_html.one: 出于安全考虑,你只能再试一次 了。
-idv.failure.phone.warning.attempts_html.other: 出于安全考虑,你只能再试%{count}次 了。
+idv.failure.phone.warning.attempts_html.other: 出于安全考虑,你只能再试%{count}次 了。
idv.failure.phone.warning.gpo.button: 通过普通邮件验证
idv.failure.phone.warning.gpo.explanation: 如果你没有别的电话号码可以尝试,那请通过普通邮件验证。
idv.failure.phone.warning.gpo.heading: 通过普通邮件验证
@@ -1134,14 +1137,14 @@ idv.messages.gpo.letter_on_the_way: 我们正在给你发信。
idv.messages.gpo.resend: 给我另外发送一封信
idv.messages.gpo.start_over_html: 如果该地址不对,你需要%{start_over_link_html}。
idv.messages.gpo.start_over_link_text: 重新开始并用你新地址进行验证。
-idv.messages.gpo.timeframe_html: 你会在 5 到 10 天 里收到带有验证码 的信。
+idv.messages.gpo.timeframe_html: 你会在 5 到 10 天 里收到带有验证码 的信。
idv.messages.otp_delivery_method_description: 如果你在上面输入的是座机电话,请在下边选择“接听电话”。
idv.messages.phone.alert_html: '输入一个符合以下条件的电话号码: '
idv.messages.phone.description: 我们会将你的号码与记录核对并给你发个一次性代码来验证你的身份。
idv.messages.phone.failed_number.alert_text: 我们无法将你与该号码匹配。
-idv.messages.phone.failed_number.gpo_alert_html: 试试 另一个 号码或者%{link_html}。
+idv.messages.phone.failed_number.gpo_alert_html: 试试 另一个 号码或者%{link_html}。
idv.messages.phone.failed_number.gpo_verify_link: 通过普通邮件验证
-idv.messages.phone.failed_number.try_again_html: 试试 另一个 号码。
+idv.messages.phone.failed_number.try_again_html: 试试 另一个 号码。
idv.messages.phone.rules:
- 美国国内电话号码
- 你的主要号码(或者你最常用的号码)
@@ -1209,8 +1212,7 @@ in_person_proofing.body.barcode.location_details: 详细地址信息
in_person_proofing.body.barcode.questions: 有问题吗?
in_person_proofing.body.barcode.retail_hours: 营业时间
in_person_proofing.body.barcode.retail_hours_closed: 关闭
-in_person_proofing.body.barcode.return_to_partner_html: 你现在可以 %{link_html}来完成你可做的任何随后步骤,直到你的身份得到验证。
-in_person_proofing.body.barcode.return_to_partner_link: 登出 %{sp_name} 并返回 %{sp_name}
+in_person_proofing.body.barcode.return_to_partner_link: 返回 %{sp_name}
in_person_proofing.body.barcode.what_to_expect: 在邮局会发生什么
in_person_proofing.body.cta.button: 尝试亲身去
in_person_proofing.body.cta.prompt_detail: 你也许可以到附近一个参与本项目的邮局去亲身验证你的身份证件。
@@ -1252,7 +1254,7 @@ in_person_proofing.body.state_id.alert_message: 州政府颁发给你的身份
in_person_proofing.body.state_id.id_types:
- 州驾照
- 州非驾照身份卡
-in_person_proofing.body.state_id.info_html: 输入 与州政府颁发的身份证件上完全一致的信息 我们将使用该信息来确认与你亲身出现所持身份证件上信息的一致性。
+in_person_proofing.body.state_id.info_html: 输入 与州政府颁发的身份证件上完全一致的信息 我们将使用该信息来确认与你亲身出现所持身份证件上信息的一致性。
in_person_proofing.body.state_id.learn_more_link: 了解更多有关哪些身份证件可被接受的信息。
in_person_proofing.body.state_id.questions: 有问题吗?
in_person_proofing.form.address.errors.unsupported_chars: 我们的系统无法读取以下字符: %{char_list}. 请替换那些字符后再试一次。
@@ -1608,7 +1610,6 @@ titles.idv.reset_password: 重设密码
titles.idv.verify_info: 验证你的信息
titles.mfa_setup.face_touch_unlock_confirmation: 人脸或触摸解锁已添加
titles.mfa_setup.suggest_second_mfa: 你已添加了第一个身份证实方法!添加第二个做备份
-titles.no_auth_option: 没找到登录方法
titles.openid_connect.authorization: OpenID Connect 授权
titles.openid_connect.logout: OpenID Connect 登出
titles.passwords.change: 更改你账户密码
@@ -1699,14 +1700,13 @@ two_factor_authentication.max_otp_requests_reached: 为了你的安全,你的
two_factor_authentication.max_personal_key_login_attempts_reached: 为了你的安全,你的账户暂时被锁住,因为你错误输入个人密钥太多次。
two_factor_authentication.max_piv_cac_login_attempts_reached: 为了你的安全,你的账户暂时被锁住,因为你错误提供 piv/cac 凭据太多次。
two_factor_authentication.mobile_terms_of_service: 移动服务条款
-two_factor_authentication.no_auth_option: 找不到身份证实选项让你登录。
two_factor_authentication.opt_in.error_retry: 抱歉,我们让你加入有困难。请再试一次。
two_factor_authentication.opt_in.opted_out_html: 你选择不在 %{phone_number_html} 接受短信。你可以选择加入并再在那个电话号码接受安全代码。
two_factor_authentication.opt_in.opted_out_last_30d_html: 你选择过去 30 天里不在 %{phone_number_html} 接受短信。我们每 30 天只能允许加入一次电话号码。
two_factor_authentication.opt_in.title: 我们无法向你电话号码发送安全代码。
two_factor_authentication.opt_in.wait_30d_opt_in: 30 天后,你可以选择加入并在那个电话号码接受安全代码。
two_factor_authentication.otp_delivery_preference.instruction: 你可以随时对此进行更改。如果你使用座机电话,请选择“接听电话”。
-two_factor_authentication.otp_delivery_preference.landline_warning_html: 输入的电话号码似乎是一个 座机电话 。通过 %{phone_setup_path} 请求一次性代码。
+two_factor_authentication.otp_delivery_preference.landline_warning_html: 输入的电话号码似乎是一个座机电话 。通过 %{phone_setup_path} 请求一次性代码。
two_factor_authentication.otp_delivery_preference.no_supported_options: 我们无法验证 %{location} 的电话号码。
two_factor_authentication.otp_delivery_preference.phone_call: 电话
two_factor_authentication.otp_delivery_preference.sms: 短信(SMS)
@@ -1844,14 +1844,14 @@ user_mailer.email_confirmation_instructions.header: '%{intro}请点击下面的
user_mailer.email_confirmation_instructions.link_text: 确认电邮地址
user_mailer.email_confirmation_instructions.subject: 确认你的电邮
user_mailer.email_deleted.header: 一个电邮地址被从你的 %{app_name} 用户资料中删除。
-user_mailer.email_deleted.help_html: 如果你没有想删除这一电邮地址, 请访问 %{app_name_html} %{help_link_html} 或者 %{contact_link_html}。
+user_mailer.email_deleted.help_html: 如果你没有想删除这一电邮地址,请访问 %{app_name_html} %{help_link_html} 或者 %{contact_link_html}。
user_mailer.email_deleted.subject: 电邮地址已删除
user_mailer.help_link_text: 帮助中心
user_mailer.in_person_completion_survey.body.cta.callout: 点击下面的按钮来开始
user_mailer.in_person_completion_survey.body.cta.label: 填写我们的意见调查
user_mailer.in_person_completion_survey.body.greeting: 你好,
user_mailer.in_person_completion_survey.body.intent: 我们想听听你在邮局亲身验证身份的经历。
-user_mailer.in_person_completion_survey.body.privacy_html: 你对这份意见调查的回复将会依照下面的 隐私和安全标准 得到保护。
+user_mailer.in_person_completion_survey.body.privacy_html: 你对这份意见调查的回复将会依照下面的 隐私和安全标准 得到保护。
user_mailer.in_person_completion_survey.body.request_description: 填写一个简短、匿名的调查问卷,你的意见会帮助我们更好满足你的需求。
user_mailer.in_person_completion_survey.body.thanks: 感谢使用 %{app_name}。
user_mailer.in_person_completion_survey.header: 花点时间告诉我们,我们表现如何
@@ -1862,7 +1862,7 @@ user_mailer.in_person_deadline_passed.body.greeting: 你好,
user_mailer.in_person_deadline_passed.body.restart: 你可以开始提出在 %{partner_agency}验证身份的新请求。
user_mailer.in_person_deadline_passed.header: 亲身验证身份的截止日期已过。
user_mailer.in_person_deadline_passed.subject: 你亲身验证身份的要求已过期。
-user_mailer.in_person_failed_suspected_fraud.body.help_center_html: 如需要更多帮助,可以访问我们的帮助中心 或寻求你试图访问的机构的帮助。
+user_mailer.in_person_failed_suspected_fraud.body.help_center_html: 如需要更多帮助,可以访问我们的帮助中心 或寻求你试图访问的机构的帮助。
user_mailer.in_person_failed_suspected_fraud.body.intro: 我们知道你曾经试图通过 %{app_name} 验证身份,但是你的身份于 %{date}在 %{location} 邮局未能得到验证。
user_mailer.in_person_failed_suspected_fraud.greeting: 你好,
user_mailer.in_person_failed_suspected_fraud.subject: 你的身份未能亲身被验证。
@@ -1918,10 +1918,10 @@ user_mailer.new_device_sign_in_before_2fa.subject: 你 %{app_name} 账户有新
user_mailer.password_changed.disavowal_link: 重设你的密码
user_mailer.password_changed.help_html: 如果你没做此更改, %{disavowal_link_html}。要得到更多帮助,请访问 %{app_name_html} %{help_link_html} 或者 %{contact_link_html}。
user_mailer.password_changed.intro_html: 你的 %{app_name_html} 账户有了新密码。
-user_mailer.personal_key_regenerated.help_html: 你的 %{app_name} 账户刚得到了一个新的 16 字符的个人密钥。你收到这一电邮是为了确保就是你。
如果你刚刚登入并重设了个人密钥,没问题!你无需做任何事情。
如果你刚才没有重设个人密钥,或者你不确定,请立即采取以下步骤来保护你账户安全:
更改你的密码 。 选择一个你在该账户没用过的密码。登录你的 %{app_name} 账户 并确保你账户页面上的信息你都能认出,包括你进行双因素身份证实的方法,比如电话号码、身份证实 app 或安全密钥。在你的%{app_name} 账户页面 , 请求新个人密钥。 请记住,除非你在用该密钥登录一个使用 %{app_name} 的受到信任的网站,绝对不要将其与人分享。 谢谢, %{app_name} 团队
+user_mailer.personal_key_regenerated.help_html: 你的 %{app_name} 账户刚得到了一个新的 16 字符的个人密钥。你收到这一电邮是为了确保就是你。
如果你刚刚登入并重设了个人密钥,没问题!你无需做任何事情。
如果你刚才没有重设个人密钥,或者你不确定,请立即采取以下步骤来保护你账户安全:
更改你的密码 。 选择一个你在该账户没用过的密码。登录你的 %{app_name} 账户 并确保你账户页面上的信息你都能认出,包括你进行双因素身份证实的方法,比如电话号码、身份证实 app 或安全密钥。在你的%{app_name} 账户页面 ,请求新个人密钥。 请记住,除非你在用该密钥登录一个使用 %{app_name} 的受到信任的网站,绝对不要将其与人分享。 谢谢, %{app_name} 团队
user_mailer.personal_key_regenerated.intro: 新个人密钥已发放
user_mailer.personal_key_regenerated.subject: 账户安全警告
-user_mailer.personal_key_sign_in.help_html: 你的 16 字符的个人密钥刚被用来登录你的 %{app_name} 账户。你收到这一电邮是为了确保就是你。
如果你刚用自己的个人密钥登入,没问题!你无需做任何事情。
如果你没用个人密钥登录,或者你不确定,请立即采取以下步骤来保护你账户安全:
更改你的密码 。 选择一个你在该账户没用过的密码。 登录你的 %{app_name} 账户 并确保你账户页面上的信息你都能认出,包括你进行双因素身份证实的方法,比如电话号码、身份证实 app 或者安全密钥。在你的%{app_name} 账户页面 , 请求新个人密钥。 请记住,除非你在用密钥登入一个使用%{app_name}的受到信任的网站,绝对不要将其与人分享。 谢谢, %{app_name} 团队
+user_mailer.personal_key_sign_in.help_html: 你的 16 字符的个人密钥刚被用来登录你的 %{app_name} 账户。你收到这一电邮是为了确保就是你。
如果你刚用自己的个人密钥登入,没问题!你无需做任何事情。
如果你没用个人密钥登录,或者你不确定,请立即采取以下步骤来保护你账户安全:
更改你的密码 。 选择一个你在该账户没用过的密码。 登录你的 %{app_name} 账户 并确保你账户页面上的信息你都能认出,包括你进行双因素身份证实的方法,比如电话号码、身份证实 app 或者安全密钥。在你的%{app_name} 账户页面 ,请求新个人密钥。 请记住,除非你在用密钥登入一个使用%{app_name}的受到信任的网站,绝对不要将其与人分享。 谢谢, %{app_name} 团队
user_mailer.personal_key_sign_in.intro: 个人密钥被用来登录
user_mailer.personal_key_sign_in.subject: 账户安全警告
user_mailer.phone_added.disavowal_link: 重设你的密码
diff --git a/db/primary_migrate/20241015154109_create_socure_reason_codes.rb b/db/primary_migrate/20241015154109_create_socure_reason_codes.rb
new file mode 100644
index 00000000000..e91a1aa9e61
--- /dev/null
+++ b/db/primary_migrate/20241015154109_create_socure_reason_codes.rb
@@ -0,0 +1,16 @@
+class CreateSocureReasonCodes < ActiveRecord::Migration[7.1]
+ def change
+ create_table :socure_reason_codes do |t|
+ t.string :code, comment: 'sensitive=false'
+ t.string :group, comment: 'sensitive=false'
+ t.text :description, comment: 'sensitive=false'
+ t.datetime :added_at, comment: 'sensitive=false'
+ t.datetime :deactivated_at, comment: 'sensitive=false'
+
+ t.timestamps comment: 'sensitive=false'
+
+ t.index :code, unique: true
+ t.index :deactivated_at
+ end
+ end
+end
diff --git a/db/primary_migrate/20241017153042_rename_socure_docv_token_in_document_capture_session.rb b/db/primary_migrate/20241017153042_rename_socure_docv_token_in_document_capture_session.rb
new file mode 100644
index 00000000000..ed63ffad9e0
--- /dev/null
+++ b/db/primary_migrate/20241017153042_rename_socure_docv_token_in_document_capture_session.rb
@@ -0,0 +1,5 @@
+class RenameSocureDocvTokenInDocumentCaptureSession < ActiveRecord::Migration[7.1]
+ def change
+ safety_assured { rename_column :document_capture_sessions, :socure_docv_token, :socure_docv_transaction_token }
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ed1586ab63f..a20b704ba51 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_01_193936) do
+ActiveRecord::Schema[7.1].define(version: 2024_10_17_153042) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "pg_stat_statements"
@@ -191,7 +191,7 @@
t.datetime "cancelled_at", precision: nil, comment: "sensitive=false"
t.boolean "ocr_confirmation_pending", default: false, comment: "sensitive=false"
t.string "last_doc_auth_result", comment: "sensitive=false"
- t.string "socure_docv_token", comment: "sensitive=false"
+ t.string "socure_docv_transaction_token", 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"
@@ -560,6 +560,18 @@
t.index ["user_id", "service_provider"], name: "index_sign_in_restrictions_on_user_id_and_service_provider", unique: true
end
+ create_table "socure_reason_codes", force: :cascade do |t|
+ t.string "code", comment: "sensitive=false"
+ t.string "group", comment: "sensitive=false"
+ t.text "description", comment: "sensitive=false"
+ t.datetime "added_at", comment: "sensitive=false"
+ t.datetime "deactivated_at", comment: "sensitive=false"
+ t.datetime "created_at", null: false, comment: "sensitive=false"
+ t.datetime "updated_at", null: false, comment: "sensitive=false"
+ t.index ["code"], name: "index_socure_reason_codes_on_code", unique: true
+ t.index ["deactivated_at"], name: "index_socure_reason_codes_on_deactivated_at"
+ end
+
create_table "sp_costs", force: :cascade do |t|
t.string "issuer", null: false, comment: "sensitive=false"
t.integer "agency_id", null: false, comment: "sensitive=false"
diff --git a/db/worker_jobs_migrate/20241021192429_recreate_good_job_cron_indexes_with_conditional.rb b/db/worker_jobs_migrate/20241021192429_recreate_good_job_cron_indexes_with_conditional.rb
new file mode 100644
index 00000000000..56cab35c385
--- /dev/null
+++ b/db/worker_jobs_migrate/20241021192429_recreate_good_job_cron_indexes_with_conditional.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class RecreateGoodJobCronIndexesWithConditional < ActiveRecord::Migration[7.1]
+ disable_ddl_transaction!
+
+ def change
+ reversible do |dir|
+ dir.up do
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at_cond)
+ add_index :good_jobs, [:cron_key, :created_at], where: "(cron_key IS NOT NULL)",
+ name: :index_good_jobs_on_cron_key_and_created_at_cond, algorithm: :concurrently
+ end
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at_cond)
+ add_index :good_jobs, [:cron_key, :cron_at], where: "(cron_key IS NOT NULL)", unique: true,
+ name: :index_good_jobs_on_cron_key_and_cron_at_cond, algorithm: :concurrently
+ end
+
+ if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at)
+ remove_index :good_jobs, name: :index_good_jobs_on_cron_key_and_created_at
+ end
+ if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at)
+ remove_index :good_jobs, name: :index_good_jobs_on_cron_key_and_cron_at
+ end
+ end
+
+ dir.down do
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at)
+ add_index :good_jobs, [:cron_key, :created_at],
+ name: :index_good_jobs_on_cron_key_and_created_at, algorithm: :concurrently
+ end
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at)
+ add_index :good_jobs, [:cron_key, :cron_at], unique: true,
+ name: :index_good_jobs_on_cron_key_and_cron_at, algorithm: :concurrently
+ end
+
+ if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at_cond)
+ remove_index :good_jobs, name: :index_good_jobs_on_cron_key_and_created_at_cond
+ end
+ if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at_cond)
+ remove_index :good_jobs, name: :index_good_jobs_on_cron_key_and_cron_at_cond
+ end
+ end
+ end
+ end
+end
diff --git a/db/worker_jobs_migrate/20241021192430_create_good_job_labels.rb b/db/worker_jobs_migrate/20241021192430_create_good_job_labels.rb
new file mode 100644
index 00000000000..1ba514e8b4a
--- /dev/null
+++ b/db/worker_jobs_migrate/20241021192430_create_good_job_labels.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class CreateGoodJobLabels < ActiveRecord::Migration[7.1]
+ def change
+ reversible do |dir|
+ dir.up do
+ # Ensure this incremental update migration is idempotent
+ # with monolithic install migration.
+ return if connection.column_exists?(:good_jobs, :labels)
+ end
+ end
+
+ add_column :good_jobs, :labels, :text, array: true
+ end
+end
diff --git a/db/worker_jobs_migrate/20241021192431_create_good_job_labels_index.rb b/db/worker_jobs_migrate/20241021192431_create_good_job_labels_index.rb
new file mode 100644
index 00000000000..65dedd477d8
--- /dev/null
+++ b/db/worker_jobs_migrate/20241021192431_create_good_job_labels_index.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class CreateGoodJobLabelsIndex < ActiveRecord::Migration[7.1]
+ disable_ddl_transaction!
+
+ def change
+ reversible do |dir|
+ dir.up do
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_labels)
+ add_index :good_jobs, :labels, using: :gin, where: "(labels IS NOT NULL)",
+ name: :index_good_jobs_on_labels, algorithm: :concurrently
+ end
+ end
+
+ dir.down do
+ if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_labels)
+ remove_index :good_jobs, name: :index_good_jobs_on_labels
+ end
+ end
+ end
+ end
+end
diff --git a/db/worker_jobs_migrate/20241021192432_remove_good_job_active_id_index.rb b/db/worker_jobs_migrate/20241021192432_remove_good_job_active_id_index.rb
new file mode 100644
index 00000000000..8601f071aa5
--- /dev/null
+++ b/db/worker_jobs_migrate/20241021192432_remove_good_job_active_id_index.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class RemoveGoodJobActiveIdIndex < ActiveRecord::Migration[7.1]
+ disable_ddl_transaction!
+
+ def change
+ reversible do |dir|
+ dir.up do
+ if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_active_job_id)
+ remove_index :good_jobs, name: :index_good_jobs_on_active_job_id
+ end
+ end
+
+ dir.down do
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_active_job_id)
+ add_index :good_jobs, :active_job_id, name: :index_good_jobs_on_active_job_id
+ end
+ end
+ end
+ end
+end
diff --git a/db/worker_jobs_migrate/20241021192433_create_index_good_job_jobs_for_candidate_lookup.rb b/db/worker_jobs_migrate/20241021192433_create_index_good_job_jobs_for_candidate_lookup.rb
new file mode 100644
index 00000000000..70e525626e9
--- /dev/null
+++ b/db/worker_jobs_migrate/20241021192433_create_index_good_job_jobs_for_candidate_lookup.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class CreateIndexGoodJobJobsForCandidateLookup < ActiveRecord::Migration[7.1]
+ disable_ddl_transaction!
+
+ def change
+ reversible do |dir|
+ dir.up do
+ # Ensure this incremental update migration is idempotent
+ # with monolithic install migration.
+ return if connection.index_name_exists?(:good_jobs, :index_good_job_jobs_for_candidate_lookup)
+ end
+ end
+
+ add_index :good_jobs, [:priority, :created_at], order: { priority: "ASC NULLS LAST", created_at: :asc },
+ where: "finished_at IS NULL", name: :index_good_job_jobs_for_candidate_lookup,
+ algorithm: :concurrently
+ end
+end
diff --git a/db/worker_jobs_migrate/20241021192434_create_good_job_execution_error_backtrace.rb b/db/worker_jobs_migrate/20241021192434_create_good_job_execution_error_backtrace.rb
new file mode 100644
index 00000000000..6b054b64060
--- /dev/null
+++ b/db/worker_jobs_migrate/20241021192434_create_good_job_execution_error_backtrace.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class CreateGoodJobExecutionErrorBacktrace < ActiveRecord::Migration[7.1]
+ def change
+ reversible do |dir|
+ dir.up do
+ # Ensure this incremental update migration is idempotent
+ # with monolithic install migration.
+ return if connection.column_exists?(:good_job_executions, :error_backtrace)
+ end
+ end
+
+ add_column :good_job_executions, :error_backtrace, :text, array: true
+ end
+end
diff --git a/db/worker_jobs_migrate/20241021192435_create_good_job_process_lock_ids.rb b/db/worker_jobs_migrate/20241021192435_create_good_job_process_lock_ids.rb
new file mode 100644
index 00000000000..f1b70a8f2e4
--- /dev/null
+++ b/db/worker_jobs_migrate/20241021192435_create_good_job_process_lock_ids.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class CreateGoodJobProcessLockIds < ActiveRecord::Migration[7.1]
+ def change
+ reversible do |dir|
+ dir.up do
+ # Ensure this incremental update migration is idempotent
+ # with monolithic install migration.
+ return if connection.column_exists?(:good_jobs, :locked_by_id)
+ end
+ end
+
+ add_column :good_jobs, :locked_by_id, :uuid
+ add_column :good_jobs, :locked_at, :datetime
+ add_column :good_job_executions, :process_id, :uuid
+ add_column :good_job_processes, :lock_type, :integer, limit: 2
+ end
+end
diff --git a/db/worker_jobs_migrate/20241021192436_create_good_job_process_lock_indexes.rb b/db/worker_jobs_migrate/20241021192436_create_good_job_process_lock_indexes.rb
new file mode 100644
index 00000000000..331825f7424
--- /dev/null
+++ b/db/worker_jobs_migrate/20241021192436_create_good_job_process_lock_indexes.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class CreateGoodJobProcessLockIndexes < ActiveRecord::Migration[7.1]
+ disable_ddl_transaction!
+
+ def change
+ reversible do |dir|
+ dir.up do
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked)
+ add_index :good_jobs, [:priority, :scheduled_at],
+ order: { priority: "ASC NULLS LAST", scheduled_at: :asc },
+ where: "finished_at IS NULL AND locked_by_id IS NULL",
+ name: :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked,
+ algorithm: :concurrently
+ end
+
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_locked_by_id)
+ add_index :good_jobs, :locked_by_id,
+ where: "locked_by_id IS NOT NULL",
+ name: :index_good_jobs_on_locked_by_id,
+ algorithm: :concurrently
+ end
+
+ unless connection.index_name_exists?(:good_job_executions, :index_good_job_executions_on_process_id_and_created_at)
+ add_index :good_job_executions, [:process_id, :created_at],
+ name: :index_good_job_executions_on_process_id_and_created_at,
+ algorithm: :concurrently
+ end
+ end
+
+ dir.down do
+ remove_index(:good_jobs, name: :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked) if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked)
+ remove_index(:good_jobs, name: :index_good_jobs_on_locked_by_id) if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_locked_by_id)
+ remove_index(:good_job_executions, name: :index_good_job_executions_on_process_id_and_created_at) if connection.index_name_exists?(:good_job_executions, :index_good_job_executions_on_process_id_and_created_at)
+ end
+ end
+ end
+end
diff --git a/db/worker_jobs_migrate/20241021192437_create_good_job_execution_duration.rb b/db/worker_jobs_migrate/20241021192437_create_good_job_execution_duration.rb
new file mode 100644
index 00000000000..fef37f07bc1
--- /dev/null
+++ b/db/worker_jobs_migrate/20241021192437_create_good_job_execution_duration.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class CreateGoodJobExecutionDuration < ActiveRecord::Migration[7.1]
+ def change
+ reversible do |dir|
+ dir.up do
+ # Ensure this incremental update migration is idempotent
+ # with monolithic install migration.
+ return if connection.column_exists?(:good_job_executions, :duration)
+ end
+ end
+
+ add_column :good_job_executions, :duration, :interval
+ end
+end
diff --git a/db/worker_jobs_schema.rb b/db/worker_jobs_schema.rb
index 3fc1dde2143..3398c6bca59 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: 2023_10_18_163221) do
+ActiveRecord::Schema[7.1].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"
@@ -41,13 +41,18 @@
t.datetime "finished_at"
t.text "error"
t.integer "error_event", limit: 2
+ t.text "error_backtrace", array: true
+ t.uuid "process_id"
+ t.interval "duration"
t.index ["active_job_id", "created_at"], name: "index_good_job_executions_on_active_job_id_and_created_at"
+ t.index ["process_id", "created_at"], name: "index_good_job_executions_on_process_id_and_created_at"
end
create_table "good_job_processes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.jsonb "state"
+ t.integer "lock_type", limit: 2
end
create_table "good_job_settings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
@@ -79,15 +84,21 @@
t.integer "executions_count"
t.text "job_class"
t.integer "error_event", limit: 2
+ t.text "labels", array: true
+ t.uuid "locked_by_id"
+ t.datetime "locked_at"
t.index ["active_job_id", "created_at"], name: "index_good_jobs_on_active_job_id_and_created_at"
- t.index ["active_job_id"], name: "index_good_jobs_on_active_job_id"
t.index ["batch_callback_id"], name: "index_good_jobs_on_batch_callback_id", where: "(batch_callback_id IS NOT NULL)"
t.index ["batch_id"], name: "index_good_jobs_on_batch_id", where: "(batch_id IS NOT NULL)"
t.index ["concurrency_key"], name: "index_good_jobs_on_concurrency_key_when_unfinished", where: "(finished_at IS NULL)"
- t.index ["cron_key", "created_at"], name: "index_good_jobs_on_cron_key_and_created_at"
- t.index ["cron_key", "cron_at"], name: "index_good_jobs_on_cron_key_and_cron_at", unique: true
+ t.index ["cron_key", "created_at"], name: "index_good_jobs_on_cron_key_and_created_at_cond", where: "(cron_key IS NOT NULL)"
+ t.index ["cron_key", "cron_at"], name: "index_good_jobs_on_cron_key_and_cron_at_cond", unique: true, where: "(cron_key IS NOT NULL)"
t.index ["finished_at"], name: "index_good_jobs_jobs_on_finished_at", where: "((retried_good_job_id IS NULL) AND (finished_at IS NOT NULL))"
+ t.index ["labels"], name: "index_good_jobs_on_labels", where: "(labels IS NOT NULL)", using: :gin
+ t.index ["locked_by_id"], name: "index_good_jobs_on_locked_by_id", where: "(locked_by_id IS NOT NULL)"
+ t.index ["priority", "created_at"], name: "index_good_job_jobs_for_candidate_lookup", where: "(finished_at IS NULL)"
t.index ["priority", "created_at"], name: "index_good_jobs_jobs_on_priority_created_at_when_unfinished", order: { priority: "DESC NULLS LAST" }, where: "(finished_at IS NULL)"
+ t.index ["priority", "scheduled_at"], name: "index_good_jobs_on_priority_scheduled_at_unfinished_unlocked", where: "((finished_at IS NULL) AND (locked_by_id IS NULL))"
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
diff --git a/dockerfiles/application.yaml b/dockerfiles/application.yaml
index 85aaf229651..94447e9e008 100644
--- a/dockerfiles/application.yaml
+++ b/dockerfiles/application.yaml
@@ -81,6 +81,18 @@ spec:
- op: add
path: /data/REDIS_IRS_ATTEMPTS_API_URL
value: "redis://{{ENVIRONMENT}}-redis.review-apps:6379/2"
+ - op: add
+ path: /data/RAILS_OFFLINE
+ value: "true"
+ - op: add
+ path: /data/LOGIN_DATACENTER
+ value: "true"
+ - op: add
+ path: /data/LOGIN_ENV
+ value: "review-app"
+ - op: add
+ path: /data/LOGIN_DOMAIN
+ value: "identitysandbox.gov"
- target:
kind: ConfigMap
name: idp-config-dbsetup
@@ -142,6 +154,18 @@ spec:
- op: add
path: /data/REDIS_IRS_ATTEMPTS_API_URL
value: "redis://{{ENVIRONMENT}}-redis.review-apps:6379/2"
+ - op: add
+ path: /data/RAILS_OFFLINE
+ value: "true"
+ - op: add
+ path: /data/LOGIN_DATACENTER
+ value: "true"
+ - op: add
+ path: /data/LOGIN_ENV
+ value: "review-app"
+ - op: add
+ path: /data/LOGIN_DOMAIN
+ value: "identitysandbox.gov"
# Patch ConfigMap for Worker
- target:
kind: ConfigMap
diff --git a/lib/identity_config.rb b/lib/identity_config.rb
index 1fd5df98fcf..6e0b04b2a07 100644
--- a/lib/identity_config.rb
+++ b/lib/identity_config.rb
@@ -122,7 +122,6 @@ def self.store
config.add(:doc_auth_max_capture_attempts_before_native_camera, type: :integer)
config.add(:doc_auth_max_submission_attempts_before_native_camera, type: :integer)
config.add(:doc_auth_selfie_desktop_test_mode, type: :boolean)
- config.add(:doc_auth_separate_pages_enabled, type: :boolean)
config.add(:doc_auth_supported_country_codes, type: :json)
config.add(:doc_auth_vendor, type: :string)
config.add(:doc_auth_vendor_default, type: :string)
@@ -175,6 +174,7 @@ def self.store
config.add(:idv_min_age_years, type: :integer)
config.add(:idv_send_link_attempt_window_in_minutes, type: :integer)
config.add(:idv_send_link_max_attempts, type: :integer)
+ config.add(:idv_socure_reason_code_download_enabled, type: :boolean)
config.add(:idv_socure_shadow_mode_enabled, type: :boolean)
config.add(:idv_sp_required, type: :boolean)
config.add(:in_person_completion_survey_url, type: :string)
@@ -372,9 +372,6 @@ def self.store
config.add(:s3_reports_enabled, type: :boolean)
config.add(:saml_endpoint_configs, type: :json, options: { symbolize_names: true })
config.add(:saml_secret_rotation_enabled, type: :boolean)
- config.add(:socure_idplus_api_key, type: :string)
- config.add(:socure_idplus_base_url, type: :string)
- config.add(:socure_idplus_timeout_in_seconds, type: :integer)
config.add(:scrypt_cost, type: :string)
config.add(:second_mfa_reminder_account_age_in_days, type: :integer)
config.add(:second_mfa_reminder_sign_in_count, type: :integer)
@@ -397,11 +394,17 @@ def self.store
config.add(:sign_in_user_id_per_ip_attempt_window_in_minutes, type: :integer)
config.add(:sign_in_user_id_per_ip_attempt_window_max_minutes, type: :integer)
config.add(:sign_in_user_id_per_ip_max_attempts, type: :integer)
+ config.add(:sign_in_recaptcha_log_failures_only, type: :boolean)
config.add(:sign_in_recaptcha_percent_tested, type: :integer)
config.add(:sign_in_recaptcha_score_threshold, type: :float)
config.add(:skip_encryption_allowed_list, type: :json)
config.add(:socure_document_request_endpoint, type: :string)
config.add(:socure_idplus_api_key, type: :string)
+ config.add(:socure_idplus_base_url, type: :string)
+ config.add(:socure_idplus_timeout_in_seconds, type: :integer)
+ config.add(:socure_reason_code_api_key, type: :string)
+ config.add(:socure_reason_code_base_url, type: :string)
+ config.add(:socure_reason_code_timeout_in_seconds, type: :integer)
config.add(:socure_webhook_enabled, type: :boolean)
config.add(:socure_enabled, type: :boolean)
config.add(:socure_webhook_secret_key, type: :string)
diff --git a/lib/identity_cors.rb b/lib/identity_cors.rb
index 61cb1b8716e..e3b26b63a92 100644
--- a/lib/identity_cors.rb
+++ b/lib/identity_cors.rb
@@ -11,7 +11,7 @@ class IdentityCors
].freeze
def self.allowed_origins_static_sites
- return STATIC_SITE_ALLOWED_ORIGINS unless Rails.env.development? || Rails.env.test?
+ return STATIC_SITE_ALLOWED_ORIGINS unless Rails.env.local?
allowed_origins = STATIC_SITE_ALLOWED_ORIGINS.dup
allowed_origins << %r{https?://localhost(:\d+)?\z}
allowed_origins << %r{https?://127\.0\.0\.1(:\d+)?\z}
diff --git a/lib/identity_job_log_subscriber.rb b/lib/identity_job_log_subscriber.rb
index baaad9fc29f..e9d981dd531 100644
--- a/lib/identity_job_log_subscriber.rb
+++ b/lib/identity_job_log_subscriber.rb
@@ -163,6 +163,8 @@ def error_or_warn(
def default_attributes(event, job)
{
duration_ms: event.duration,
+ cpu_time_ms: event.cpu_time,
+ idle_time_ms: event.idle_time,
timestamp: Time.zone.now,
name: event.name,
job_class: job.class.name,
diff --git a/lib/reporting/authentication_report.rb b/lib/reporting/authentication_report.rb
index e2dfe27eba9..eac43743d0d 100644
--- a/lib/reporting/authentication_report.rb
+++ b/lib/reporting/authentication_report.rb
@@ -211,7 +211,6 @@ def format_as_percent(numerator:, denominator:)
end
end
-# rubocop:disable Rails/Output
if __FILE__ == $PROGRAM_NAME
options = Reporting::CommandLineOptions.new.parse!(ARGV)
@@ -219,4 +218,3 @@ def format_as_percent(numerator:, denominator:)
puts csv
end
end
-# rubocop:enable Rails/Output
diff --git a/lib/reporting/drop_off_report.rb b/lib/reporting/drop_off_report.rb
index 6aaf6f4d2d9..7b6b45336c6 100644
--- a/lib/reporting/drop_off_report.rb
+++ b/lib/reporting/drop_off_report.rb
@@ -450,7 +450,6 @@ def cloudwatch_client
end
end
-# rubocop:disable Rails/Output
if __FILE__ == $PROGRAM_NAME
options = Reporting::CommandLineOptions.new.parse!(ARGV, require_issuer: false)
@@ -458,4 +457,3 @@ def cloudwatch_client
puts csv
end
end
-# rubocop:enable Rails/Output
diff --git a/lib/reporting/mfa_report.rb b/lib/reporting/mfa_report.rb
index 6bfcc4b66ce..79e3fbeb69d 100644
--- a/lib/reporting/mfa_report.rb
+++ b/lib/reporting/mfa_report.rb
@@ -182,7 +182,6 @@ def multi_factor_auth_table
end
end
-# rubocop:disable Rails/Output
if __FILE__ == $PROGRAM_NAME
options = Reporting::CommandLineOptions.new.parse!(ARGV)
@@ -190,4 +189,3 @@ def multi_factor_auth_table
puts csv
end
end
-# rubocop:enable Rails/Output
diff --git a/lib/reporting/protocols_report.rb b/lib/reporting/protocols_report.rb
index b68d3ef69ad..c7ee1e2a382 100644
--- a/lib/reporting/protocols_report.rb
+++ b/lib/reporting/protocols_report.rb
@@ -72,6 +72,10 @@ def as_emailable_reports
title: 'Deprecated Parameter Usage',
table: deprecated_parameters_table,
),
+ Reporting::EmailableReport.new(
+ title: 'Feature Usage',
+ table: feature_use_table,
+ ),
]
end
@@ -371,7 +375,6 @@ def to_percent(numerator, denominator)
end
end
-# rubocop:disable Rails/Output
if __FILE__ == $PROGRAM_NAME
options = Reporting::CommandLineOptions.new.parse!(ARGV, require_issuer: false)
@@ -379,4 +382,3 @@ def to_percent(numerator, denominator)
puts csv
end
end
-# rubocop:enable Rails/Output
diff --git a/scripts/enforce-typescript-files.mjs b/scripts/enforce-typescript-files.mjs
index 90341fe763f..f75d5f728bd 100755
--- a/scripts/enforce-typescript-files.mjs
+++ b/scripts/enforce-typescript-files.mjs
@@ -9,8 +9,6 @@ import glob from 'fast-glob';
// only ever shrink over time. Scripts which are loaded directly by Node.js should exist within
// packages with a defined entrypoint.
const LEGACY_FILE_EXCEPTIONS = [
- 'app/javascript/packages/compose-components/index.js',
- 'app/javascript/packages/compose-components/index.spec.jsx',
'app/javascript/packages/device/index.js',
'app/javascript/packages/document-capture/index.js',
'app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx',
diff --git a/spec/controllers/idv/hybrid_mobile/entry_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/entry_controller_spec.rb
index 1a5f2727c03..8d43b38e37f 100644
--- a/spec/controllers/idv/hybrid_mobile/entry_controller_spec.rb
+++ b/spec/controllers/idv/hybrid_mobile/entry_controller_spec.rb
@@ -15,7 +15,6 @@
let(:idv_vendor) { Idp::Constants::Vendors::MOCK }
before do
- stub_analytics
allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return(idv_vendor)
allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return(idv_vendor)
end
@@ -35,15 +34,6 @@
get :show, params: { 'document-capture-session': 'foo' }
end
- it 'logs an analytics event' do
- expect(@analytics).to have_logged_event(
- 'Doc Auth',
- hash_including(
- success: false,
- errors: { session_uuid: ['invalid session'] },
- ),
- )
- end
it 'redirects to the root url' do
expect(response).to redirect_to root_url
end
@@ -78,16 +68,6 @@
it 'redirects to the first step' do
expect(response).to redirect_to idv_hybrid_mobile_socure_document_capture_url
end
-
- it 'logs an analytics event' do
- expect(@analytics).to have_logged_event(
- 'Doc Auth',
- hash_including(
- success: true,
- doc_capture_user_id?: false,
- ),
- )
- end
end
context 'doc auth vendor is lexis nexis' do
@@ -96,16 +76,6 @@
it 'redirects to the first step' do
expect(response).to redirect_to idv_hybrid_mobile_document_capture_url
end
-
- it 'logs an analytics event' do
- expect(@analytics).to have_logged_event(
- 'Doc Auth',
- hash_including(
- success: true,
- doc_capture_user_id?: false,
- ),
- )
- end
end
context 'but we already had a session' do
@@ -127,16 +97,6 @@
expect(controller.session).to include(document_capture_session_uuid: session_uuid)
end
- it 'logs an analytics event' do
- expect(@analytics).to have_logged_event(
- 'Doc Auth',
- hash_including(
- success: true,
- doc_capture_user_id?: true,
- ),
- )
- end
-
context 'doc auth vendor is socure' do
let(:idv_vendor) { Idp::Constants::Vendors::SOCURE }
diff --git a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb
index 7d3bf05ca5f..8cee30758b4 100644
--- a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb
+++ b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb
@@ -150,7 +150,8 @@
it 'puts the docvTransactionToken into the document capture session' do
document_capture_session.reload
- expect(document_capture_session.socure_docv_token).to eq(docv_transaction_token)
+ expect(document_capture_session.socure_docv_transaction_token).
+ to eq(docv_transaction_token)
end
end
end
diff --git a/spec/controllers/idv/socure/document_capture_controller_spec.rb b/spec/controllers/idv/socure/document_capture_controller_spec.rb
index e2fcfa51a1c..863f553eb3e 100644
--- a/spec/controllers/idv/socure/document_capture_controller_spec.rb
+++ b/spec/controllers/idv/socure/document_capture_controller_spec.rb
@@ -152,7 +152,8 @@
it 'puts the docvTransactionToken into the document capture session' do
document_capture_session.reload
- expect(document_capture_session.socure_docv_token).to eq(docv_transaction_token)
+ expect(document_capture_session.socure_docv_transaction_token).
+ to eq(docv_transaction_token)
end
end
end
diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb
index 4319aa75a17..9f453c60677 100644
--- a/spec/controllers/idv/verify_info_controller_spec.rb
+++ b/spec/controllers/idv/verify_info_controller_spec.rb
@@ -149,6 +149,7 @@
context: {
stages: {
threatmetrix: {
+ client: threatmetrix_client_id,
transaction_id: 1,
review_status: review_status,
response_body: {
@@ -226,9 +227,7 @@
context: hash_including(
stages: hash_including(
threatmetrix: hash_including(
- response_body: hash_including(
- client: threatmetrix_client_id,
- ),
+ client: threatmetrix_client_id,
),
),
),
@@ -390,6 +389,10 @@
},
),
)
+
+ event = @analytics.events['IdV: doc auth verify proofing results'].first
+ state_id = event.dig(:proofing_results, :context, :stages, :state_id)
+ expect(state_id).to match(a_hash_including(state_id_type: 'drivers_license'))
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 74b204390d2..3656a2d72ac 100644
--- a/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb
+++ b/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb
@@ -12,76 +12,72 @@
end
end
- describe 'when not signed in' do
- describe 'GET index' do
+ 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
- end
- describe 'when signed out' do
- describe 'GET index' do
+ 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
- end
- describe 'when signing in' do
- before(:each) { stub_sign_in_before_2fa(user) }
- let(:user) do
- create(:user, :fully_registered, :with_piv_or_cac, with: { phone: '+1 (703) 555-0000' })
- end
+ context 'when signing in' do
+ before { stub_sign_in_before_2fa(user) }
+
+ let(:user) do
+ create(:user, :fully_registered, :with_piv_or_cac, with: { phone: '+1 (703) 555-0000' })
+ end
- describe 'GET index' do
it 'redirects to 2fa entry' do
get :new
+
expect(response).to redirect_to(user_two_factor_authentication_url)
end
end
- end
- describe 'when signed in' do
- before(:each) { stub_sign_in(user) }
+ context 'when signed in' do
+ before { stub_sign_in(user) }
- context 'without associated piv/cac' do
- let(:user) do
- create(:user, :fully_registered, with: { phone: '+1 (703) 555-0000' })
- end
- let(:nickname) { 'Card 1' }
+ context 'without associated piv/cac' do
+ let(:user) do
+ create(:user, :fully_registered, with: { phone: '+1 (703) 555-0000' })
+ end
+ let(:nickname) { 'Card 1' }
- 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
- end
+ 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
+ end
- let(:nonce) { 'nonce' }
+ let(:nonce) { 'nonce' }
- let(:good_token) { 'good-token' }
- let(:good_token_response) do
- {
- 'subject' => 'some dn',
- 'uuid' => 'some-random-string',
- 'nonce' => nonce,
- }
- end
+ let(:good_token) { 'good-token' }
+ let(:good_token_response) do
+ {
+ 'subject' => 'some dn',
+ 'uuid' => 'some-random-string',
+ 'nonce' => nonce,
+ }
+ end
- let(:bad_token) { 'bad-token' }
- let(:bad_token_response) do
- {
- 'error' => 'certificate.bad',
- 'nonce' => nonce,
- }
- end
+ let(:bad_token) { 'bad-token' }
+ let(:bad_token_response) do
+ {
+ 'error' => 'certificate.bad',
+ 'nonce' => nonce,
+ }
+ end
- describe 'GET index' do
context 'when rendered without a token' do
it 'renders the "new" template' do
get :new
diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb
index be407001213..6a535885546 100644
--- a/spec/controllers/users/sessions_controller_spec.rb
+++ b/spec/controllers/users/sessions_controller_spec.rb
@@ -341,33 +341,55 @@
and_return(:sign_in_recaptcha)
end
- it 'tracks unsuccessful authentication for failed reCAPTCHA' do
- user = create(:user, :fully_registered)
+ context 'when configured to log failures only' do
+ before do
+ allow(IdentityConfig.store).to receive(:sign_in_recaptcha_log_failures_only).
+ and_return(true)
+ end
- stub_analytics
+ it 'redirects unsuccessful authentication for failed reCAPTCHA to failed page' do
+ user = create(:user, :fully_registered)
- post :create, params: { user: { email: user.email, password: user.password, score: 0.1 } }
+ post :create, params: { user: { email: user.email, password: user.password, score: 0.1 } }
- expect(@analytics).to have_logged_event(
- 'Email and Password Authentication',
- success: false,
- user_id: user.uuid,
- user_locked_out: false,
- rate_limited: false,
- valid_captcha_result: false,
- captcha_validation_performed: true,
- bad_password_count: 0,
- remember_device: false,
- sp_request_url_present: false,
- )
+ expect(response).to redirect_to user_two_factor_authentication_url
+ end
end
- it 'redirects unsuccessful authentication for failed reCAPTCHA to failed page' do
- user = create(:user, :fully_registered)
+ context 'when not configured to log failures only' do
+ before do
+ allow(IdentityConfig.store).to receive(:sign_in_recaptcha_log_failures_only).
+ and_return(false)
+ end
- post :create, params: { user: { email: user.email, password: user.password, score: 0.1 } }
+ it 'tracks unsuccessful authentication for failed reCAPTCHA' do
+ user = create(:user, :fully_registered)
- expect(response).to redirect_to sign_in_security_check_failed_url
+ stub_analytics
+
+ post :create, params: { user: { email: user.email, password: user.password, score: 0.1 } }
+
+ expect(@analytics).to have_logged_event(
+ 'Email and Password Authentication',
+ success: false,
+ user_id: user.uuid,
+ user_locked_out: false,
+ rate_limited: false,
+ valid_captcha_result: false,
+ captcha_validation_performed: true,
+ bad_password_count: 0,
+ remember_device: false,
+ sp_request_url_present: false,
+ )
+ end
+
+ it 'redirects unsuccessful authentication for failed reCAPTCHA to failed page' do
+ user = create(:user, :fully_registered)
+
+ post :create, params: { user: { email: user.email, password: user.password, score: 0.1 } }
+
+ expect(response).to redirect_to sign_in_security_check_failed_url
+ end
end
end
diff --git a/spec/features/account_reset/delete_account_spec.rb b/spec/features/account_reset/delete_account_spec.rb
index 8b4966e29a5..8d426766b23 100644
--- a/spec/features/account_reset/delete_account_spec.rb
+++ b/spec/features/account_reset/delete_account_spec.rb
@@ -31,6 +31,7 @@
to have_content strip_tags(
t('account_reset.request.delete_account'),
)
+
click_button t('account_reset.request.yes_continue')
expect(page).
@@ -48,7 +49,7 @@
reset_email
- travel_to(Time.zone.now + 2.days + 1) do
+ travel_to(Time.zone.now + 2.days + 2) do
AccountReset::GrantRequestsAndSendEmails.new.perform(Time.zone.today)
open_last_email
click_email_link_matching(/delete_account\?token/)
diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb
index 4c82b87b2c8..bc72841df9b 100644
--- a/spec/features/idv/analytics_spec.rb
+++ b/spec/features/idv/analytics_spec.rb
@@ -29,7 +29,6 @@
client: nil,
errors: {},
exception: nil,
- response_body: threatmetrix_response_body,
review_status: 'pass',
account_lex_id: 'super-cool-test-lex-id',
session_id: 'super-cool-test-session-id',
@@ -71,6 +70,10 @@
jurisdiction_in_maintenance_window: false }
end
+ let(:state_id_resolution_with_id_type) do
+ state_id_resolution.merge(state_id_type: 'drivers_license')
+ end
+
let(:resolution_block) do
{ success: true,
errors: {},
@@ -122,6 +125,12 @@
}
end
+ let(:doc_auth_verify_proofing_results) do
+ base_proofing_results.deep_merge(
+ context: { stages: { state_id: state_id_resolution_with_id_type } },
+ )
+ end
+
let(:in_person_path_proofing_results) do
{
exception: nil,
@@ -145,7 +154,7 @@
vendor_name: 'ResolutionMock',
vendor_workflow: nil,
verified_attributes: nil },
- state_id: state_id_resolution,
+ state_id: state_id_resolution_with_id_type,
threatmetrix: threatmetrix_response,
},
},
@@ -228,7 +237,7 @@
),
'IdV: doc auth verify proofing results' => {
success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', step: 'verify',
- proofing_results: base_proofing_results
+ proofing_results: doc_auth_verify_proofing_results
},
'IdV: phone of record visited' => {
@@ -348,7 +357,7 @@
),
'IdV: doc auth verify proofing results' => {
success: true, errors: {}, flow_path: 'hybrid', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', step: 'verify',
- proofing_results: base_proofing_results
+ proofing_results: doc_auth_verify_proofing_results
},
'IdV: phone of record visited' => {
@@ -465,7 +474,7 @@
),
'IdV: doc auth verify proofing results' => {
success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', step: 'verify',
- proofing_results: base_proofing_results
+ proofing_results: doc_auth_verify_proofing_results
},
'IdV: phone of record visited' => {
proofing_components: base_proofing_components,
@@ -702,7 +711,7 @@
),
'IdV: doc auth verify proofing results' => {
success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', step: 'verify',
- proofing_results: base_proofing_results
+ proofing_results: doc_auth_verify_proofing_results
},
'IdV: phone of record visited' => {
@@ -828,7 +837,6 @@
review_status: 'pass',
account_lex_id: nil,
session_id: nil,
- response_body: threatmetrix_response_body,
}
end
@@ -909,7 +917,6 @@
review_status: 'pass',
account_lex_id: nil,
session_id: nil,
- response_body: threatmetrix_response_body,
}
end
@@ -959,7 +966,6 @@
review_status: 'pass',
account_lex_id: nil,
session_id: nil,
- response_body: threatmetrix_response_body,
}
end
@@ -970,292 +976,84 @@
end
end
end
-
- context 'in person path' do
- let(:return_sp_url) { 'https://example.com/some/idv/ipp/url' }
-
- 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(false)
- allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).and_return(true)
- allow_any_instance_of(Idv::InPerson::ReadyToVerifyPresenter).
- to receive(:service_provider_homepage_url).and_return(return_sp_url)
- allow_any_instance_of(Idv::InPerson::ReadyToVerifyPresenter).
- to receive(:sp_name).and_return(sp_friendly_name)
- allow(IdentityConfig.store).to receive(:in_person_proofing_enforce_tmx).
- and_return(true)
-
- start_idv_from_sp(:saml)
- sign_in_and_2fa_user(user)
- begin_in_person_proofing(user)
- complete_all_in_person_proofing_steps(user, same_address_as_id: false)
- complete_phone_step(user)
- complete_enter_password_step(user)
- acknowledge_and_confirm_personal_key
- visit_help_center
- visit_sp_from_in_person_ready_to_verify
- end
-
- it 'records all of the events', allow_browser_log: true do
- max_wait = Time.zone.now + 5.seconds
- wait_for_event('IdV: user clicked what to bring link on ready to verify page', max_wait)
- wait_for_event('IdV: user clicked sp link on ready to verify page', max_wait)
- in_person_path_events.each do |event, attributes|
- expect(fake_analytics).to have_logged_event(event, attributes)
- end
- end
-
- context 'proofing_device_profiling disabled' do
- let(:proofing_device_profiling) { :disabled }
- let(:idv_level) { 'legacy_in_person' }
- let(:threatmetrix) { false }
- let(:threatmetrix_response_body) { nil }
- let(:threatmetrix_response) do
- {
- client: 'tmx_disabled',
- success: true,
- errors: {},
- exception: nil,
- timed_out: false,
- transaction_id: nil,
- review_status: 'pass',
- account_lex_id: nil,
- session_id: nil,
- response_body: threatmetrix_response_body,
- }
- end
-
- it 'records all of the events', allow_browser_log: true do
- max_wait = Time.zone.now + 5.seconds
- wait_for_event('IdV: user clicked what to bring link on ready to verify page', max_wait)
- wait_for_event('IdV: user clicked sp link on ready to verify page', max_wait)
- in_person_path_events.each do |event, attributes|
- expect(fake_analytics).to have_logged_event(event, attributes)
- end
- end
- end
-
- # wait for event to happen
- def wait_for_event(event, wait)
- frequency = 0.1.seconds
- loop do
- expect(fake_analytics).to have_logged_event(event)
- return
- rescue RSpec::Expectations::ExpectationNotMetError => err
- raise err if wait - Time.zone.now < frequency
- sleep frequency
- next
- end
- end
- end
-
- context 'Happy selfie path' do
- before do
- allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:success)
-
- perform_in_browser(:desktop) do
+ context 'Hybrid flow' do
+ context 'facial comparison not required - Happy path' do
+ before do
sign_in_and_2fa_user(user)
- visit_idp_from_sp_with_ial2(:oidc, facial_match_required: true)
- complete_doc_auth_steps_before_document_capture_step
- attach_images
- attach_selfie
- submit_images
-
- click_idv_continue
- visit idv_ssn_url
+ visit_idp_from_sp_with_ial2(:oidc)
+ complete_welcome_step
+ complete_agreement_step
+ complete_hybrid_handoff_step
+ complete_document_capture_step
complete_ssn_step
complete_verify_step
- fill_out_phone_form_ok('202-555-1212')
- verify_phone_otp
+ complete_phone_step(user)
complete_enter_password_step(user)
acknowledge_and_confirm_personal_key
end
- end
-
- it 'records all of the events' do
- happy_mobile_selfie_path_events.each do |event, attributes|
- expect(fake_analytics).to have_logged_event(event, attributes)
- end
- end
-
- context 'proofing_device_profiling disabled' do
- let(:proofing_device_profiling) { :disabled }
- let(:threatmetrix) { false }
- let(:threatmetrix_response_body) { nil }
- let(:threatmetrix_response) do
- {
- client: 'tmx_disabled',
- success: true,
- errors: {},
- exception: nil,
- timed_out: false,
- transaction_id: nil,
- review_status: 'pass',
- account_lex_id: nil,
- session_id: nil,
- response_body: threatmetrix_response_body,
- }
- end
it 'records all of the events' do
aggregate_failures 'analytics events' do
- happy_mobile_selfie_path_events.each do |event, attributes|
+ happy_path_events.each do |event, attributes|
expect(fake_analytics).to have_logged_event(event, attributes)
end
end
- end
- end
- end
- context 'doc_auth_separate_pages_enabled is true' do
- before do
- allow(IdentityConfig.store).to receive(:doc_auth_separate_pages_enabled).and_return(true)
- end
- context 'Hybrid flow' do
- context 'facial comparison not required - Happy path' do
- before do
- sign_in_and_2fa_user(user)
- visit_idp_from_sp_with_ial2(:oidc)
- complete_welcome_step
- complete_agreement_step
- complete_hybrid_handoff_step
- complete_document_capture_step
- complete_ssn_step
- complete_verify_step
- complete_phone_step(user)
- complete_enter_password_step(user)
- acknowledge_and_confirm_personal_key
- end
-
- it 'records all of the events' do
- aggregate_failures 'analytics events' do
- happy_path_events.each do |event, attributes|
- expect(fake_analytics).to have_logged_event(event, attributes)
- end
- end
-
- aggregate_failures 'populates data for each step of the Daily Dropoff Report' do
- row = CSV.parse(
- Reports::DailyDropoffsReport.new.tap do |r|
- r.report_date = Time.zone.now
- end.report_body,
- headers: true,
- ).first
-
- Reports::DailyDropoffsReport::STEPS.each do |step|
- expect(row[step].to_i).to(be > 0, "step #{step} was counted")
- end
- end
- end
- context 'proofing_device_profiling disabled' do
- let(:proofing_device_profiling) { :disabled }
- let(:threatmetrix) { false }
- let(:threatmetrix_response_body) { nil }
- let(:threatmetrix_response) do
- {
- client: 'tmx_disabled',
- success: true,
- errors: {},
- exception: nil,
- timed_out: false,
- transaction_id: nil,
- review_status: 'pass',
- account_lex_id: nil,
- session_id: nil,
- response_body: threatmetrix_response_body,
- }
- end
+ aggregate_failures 'populates data for each step of the Daily Dropoff Report' do
+ row = CSV.parse(
+ Reports::DailyDropoffsReport.new.tap do |r|
+ r.report_date = Time.zone.now
+ end.report_body,
+ headers: true,
+ ).first
- it 'records all of the events', allow_browser_log: true do
- aggregate_failures 'analytics events' do
- happy_path_events.each do |event, attributes|
- expect(fake_analytics).to have_logged_event(event, attributes)
- end
- end
+ Reports::DailyDropoffsReport::STEPS.each do |step|
+ expect(row[step].to_i).to(be > 0, "step #{step} was counted")
end
end
end
- context 'facial comparison required - Happy path' do
- before do
- allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:success)
-
- perform_in_browser(:mobile) do
- sign_in_and_2fa_user(user)
- visit_idp_from_sp_with_ial2(:oidc, facial_match_required: true)
- complete_doc_auth_steps_before_document_capture_step
- attach_images
- click_continue
- attach_selfie
- submit_images
-
- click_idv_continue
- visit idv_ssn_url
- complete_ssn_step
- complete_verify_step
- fill_out_phone_form_ok('202-555-1212')
- verify_phone_otp
- complete_enter_password_step(user)
- acknowledge_and_confirm_personal_key
- end
- end
- it 'records all of the events' do
- happy_mobile_selfie_path_events.each do |event, attributes|
- expect(fake_analytics).to have_logged_event(event, attributes)
- end
+ context 'proofing_device_profiling disabled' do
+ let(:proofing_device_profiling) { :disabled }
+ let(:threatmetrix) { false }
+ let(:threatmetrix_response_body) { nil }
+ let(:threatmetrix_response) do
+ {
+ client: 'tmx_disabled',
+ success: true,
+ errors: {},
+ exception: nil,
+ timed_out: false,
+ transaction_id: nil,
+ review_status: 'pass',
+ account_lex_id: nil,
+ session_id: nil,
+ }
end
- context 'proofing_device_profiling disabled' do
- let(:proofing_device_profiling) { :disabled }
- let(:threatmetrix) { false }
- let(:threatmetrix_response_body) { nil }
- let(:threatmetrix_response) do
- {
- client: 'tmx_disabled',
- success: true,
- errors: {},
- exception: nil,
- timed_out: false,
- transaction_id: nil,
- review_status: 'pass',
- account_lex_id: nil,
- session_id: nil,
- response_body: threatmetrix_response_body,
- }
- end
-
- it 'records all of the events' do
- aggregate_failures 'analytics events' do
- happy_mobile_selfie_path_events.each do |event, attributes|
- expect(fake_analytics).to have_logged_event(event, attributes)
- end
+ it 'records all of the events', allow_browser_log: true do
+ aggregate_failures 'analytics events' do
+ happy_path_events.each do |event, attributes|
+ expect(fake_analytics).to have_logged_event(event, attributes)
end
end
end
end
end
- context 'facial comparison not required - Happy path' do
+ context 'facial comparison required - Happy path' do
before do
- 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
-
- perform_in_browser(:desktop) do
- sign_in_and_2fa_user(user)
- visit_idp_from_sp_with_ial2(:oidc)
- complete_welcome_step
- complete_agreement_step
- click_send_link
- end
+ allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:success)
perform_in_browser(:mobile) do
- visit @sms_link
- attach_and_submit_images
- visit idv_hybrid_mobile_document_capture_url
- end
+ sign_in_and_2fa_user(user)
+ visit_idp_from_sp_with_ial2(:oidc, facial_match_required: true)
+ complete_doc_auth_steps_before_document_capture_step
+ attach_images
+ click_continue
+ click_button 'Take photo'
+ attach_selfie
+ submit_images
- perform_in_browser(:desktop) do
click_idv_continue
visit idv_ssn_url
complete_ssn_step
@@ -1268,21 +1066,8 @@ def wait_for_event(event, wait)
end
it 'records all of the events' do
- aggregate_failures 'analytics events' do
- happy_hybrid_path_events.each do |event, attributes|
- expect(fake_analytics).to have_logged_event(event, attributes)
- end
- end
-
- aggregate_failures 'populates data for each step of the Daily Dropoff Report' do
- row = CSV.parse(
- Reports::DailyDropoffsReport.new.tap { |r| r.report_date = Time.zone.now }.report_body,
- headers: true,
- ).first
-
- Reports::DailyDropoffsReport::STEPS.each do |step|
- expect(row[step].to_i).to(be > 0, "step #{step} was counted")
- end
+ happy_mobile_selfie_path_events.each do |event, attributes|
+ expect(fake_analytics).to have_logged_event(event, attributes)
end
end
@@ -1301,95 +1086,173 @@ def wait_for_event(event, wait)
review_status: 'pass',
account_lex_id: nil,
session_id: nil,
- response_body: threatmetrix_response_body,
}
end
it 'records all of the events' do
aggregate_failures 'analytics events' do
- happy_hybrid_path_events.each do |event, attributes|
+ happy_mobile_selfie_path_events.each do |event, attributes|
expect(fake_analytics).to have_logged_event(event, attributes)
end
end
end
end
end
- context 'in person path' do
- let(:return_sp_url) { 'https://example.com/some/idv/ipp/url' }
+ end
+ context 'facial comparison not required - Happy path' do
+ before do
+ 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
- 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(false)
- allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).and_return(true)
- allow_any_instance_of(Idv::InPerson::ReadyToVerifyPresenter).
- to receive(:service_provider_homepage_url).and_return(return_sp_url)
- allow_any_instance_of(Idv::InPerson::ReadyToVerifyPresenter).
- to receive(:sp_name).and_return(sp_friendly_name)
- allow(IdentityConfig.store).to receive(:in_person_proofing_enforce_tmx).
- and_return(true)
-
- start_idv_from_sp(:saml)
+ perform_in_browser(:desktop) do
sign_in_and_2fa_user(user)
- begin_in_person_proofing(user)
- complete_all_in_person_proofing_steps(user, same_address_as_id: false)
- complete_phone_step(user)
+ visit_idp_from_sp_with_ial2(:oidc)
+ complete_welcome_step
+ complete_agreement_step
+ click_send_link
+ end
+
+ perform_in_browser(:mobile) do
+ visit @sms_link
+ attach_and_submit_images
+ visit idv_hybrid_mobile_document_capture_url
+ end
+
+ perform_in_browser(:desktop) do
+ click_idv_continue
+ visit idv_ssn_url
+ complete_ssn_step
+ complete_verify_step
+ fill_out_phone_form_ok('202-555-1212')
+ verify_phone_otp
complete_enter_password_step(user)
acknowledge_and_confirm_personal_key
- visit_help_center
- visit_sp_from_in_person_ready_to_verify
end
+ end
- it 'records all of the events', allow_browser_log: true do
- max_wait = Time.zone.now + 5.seconds
- wait_for_event('IdV: user clicked what to bring link on ready to verify page', max_wait)
- wait_for_event('IdV: user clicked sp link on ready to verify page', max_wait)
- in_person_path_events.each do |event, attributes|
+ it 'records all of the events' do
+ aggregate_failures 'analytics events' do
+ happy_hybrid_path_events.each do |event, attributes|
expect(fake_analytics).to have_logged_event(event, attributes)
end
end
- context 'proofing_device_profiling disabled' do
- let(:proofing_device_profiling) { :disabled }
- let(:idv_level) { 'legacy_in_person' }
- let(:threatmetrix) { false }
- let(:threatmetrix_response_body) { nil }
- let(:threatmetrix_response) do
- {
- client: 'tmx_disabled',
- success: true,
- errors: {},
- exception: nil,
- timed_out: false,
- transaction_id: nil,
- review_status: 'pass',
- account_lex_id: nil,
- session_id: nil,
- response_body: threatmetrix_response_body,
- }
+ aggregate_failures 'populates data for each step of the Daily Dropoff Report' do
+ row = CSV.parse(
+ Reports::DailyDropoffsReport.new.tap { |r| r.report_date = Time.zone.now }.report_body,
+ headers: true,
+ ).first
+
+ Reports::DailyDropoffsReport::STEPS.each do |step|
+ expect(row[step].to_i).to(be > 0, "step #{step} was counted")
end
+ end
+ end
- it 'records all of the events', allow_browser_log: true do
- max_wait = Time.zone.now + 5.seconds
- wait_for_event('IdV: user clicked what to bring link on ready to verify page', max_wait)
- wait_for_event('IdV: user clicked sp link on ready to verify page', max_wait)
- in_person_path_events.each do |event, attributes|
+ context 'proofing_device_profiling disabled' do
+ let(:proofing_device_profiling) { :disabled }
+ let(:threatmetrix) { false }
+ let(:threatmetrix_response_body) { nil }
+ let(:threatmetrix_response) do
+ {
+ client: 'tmx_disabled',
+ success: true,
+ errors: {},
+ exception: nil,
+ timed_out: false,
+ transaction_id: nil,
+ review_status: 'pass',
+ account_lex_id: nil,
+ session_id: nil,
+ }
+ end
+
+ it 'records all of the events' do
+ aggregate_failures 'analytics events' do
+ happy_hybrid_path_events.each do |event, attributes|
expect(fake_analytics).to have_logged_event(event, attributes)
end
end
end
+ end
+ end
+
+ context 'in person path' do
+ let(:return_sp_url) { 'https://example.com/some/idv/ipp/url' }
+
+ 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(false)
+ allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).and_return(true)
+ allow_any_instance_of(Idv::InPerson::ReadyToVerifyPresenter).
+ to receive(:service_provider_homepage_url).and_return(return_sp_url)
+ allow_any_instance_of(Idv::InPerson::ReadyToVerifyPresenter).
+ to receive(:sp_name).and_return(sp_friendly_name)
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enforce_tmx).
+ and_return(true)
+
+ start_idv_from_sp(:saml)
+ sign_in_and_2fa_user(user)
+ begin_in_person_proofing(user)
+ complete_all_in_person_proofing_steps(user, same_address_as_id: false)
+ complete_phone_step(user)
+ complete_enter_password_step(user)
+ acknowledge_and_confirm_personal_key
+ visit_help_center
+ visit_sp_from_in_person_ready_to_verify
+ end
+
+ it 'records all of the events', allow_browser_log: true do
+ max_wait = Time.zone.now + 5.seconds
+ wait_for_event('IdV: user clicked what to bring link on ready to verify page', max_wait)
+ wait_for_event('IdV: user clicked sp link on ready to verify page', max_wait)
+ in_person_path_events.each do |event, attributes|
+ expect(fake_analytics).to have_logged_event(event, attributes)
+ end
+ end
- # wait for event to happen
- def wait_for_event(event, wait)
- frequency = 0.1.seconds
- loop do
- expect(fake_analytics).to have_logged_event(event)
- return
- rescue RSpec::Expectations::ExpectationNotMetError => err
- raise err if wait - Time.zone.now < frequency
- sleep frequency
- next
+ context 'proofing_device_profiling disabled' do
+ let(:proofing_device_profiling) { :disabled }
+ let(:idv_level) { 'legacy_in_person' }
+ let(:threatmetrix) { false }
+ let(:threatmetrix_response_body) { nil }
+ let(:threatmetrix_response) do
+ {
+ client: 'tmx_disabled',
+ success: true,
+ errors: {},
+ exception: nil,
+ timed_out: false,
+ transaction_id: nil,
+ review_status: 'pass',
+ account_lex_id: nil,
+ session_id: nil,
+ }
+ end
+
+ it 'records all of the events', allow_browser_log: true do
+ max_wait = Time.zone.now + 5.seconds
+ wait_for_event('IdV: user clicked what to bring link on ready to verify page', max_wait)
+ wait_for_event('IdV: user clicked sp link on ready to verify page', max_wait)
+ in_person_path_events.each do |event, attributes|
+ expect(fake_analytics).to have_logged_event(event, attributes)
end
end
end
+
+ # wait for event to happen
+ def wait_for_event(event, wait)
+ frequency = 0.1.seconds
+ loop do
+ expect(fake_analytics).to have_logged_event(event)
+ return
+ rescue RSpec::Expectations::ExpectationNotMetError => err
+ raise err if wait - Time.zone.now < frequency
+ sleep frequency
+ next
+ end
+ end
end
end
diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb
index 8b5f3f9d4ea..dd1f0adb23a 100644
--- a/spec/features/idv/doc_auth/document_capture_spec.rb
+++ b/spec/features/idv/doc_auth/document_capture_spec.rb
@@ -131,6 +131,161 @@
end
end
+ context 'facial match is required', allow_browser_log: true do
+ before do
+ allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
+ visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
+ sign_in_and_2fa_user(@user)
+ complete_doc_auth_steps_before_document_capture_step
+ end
+
+ it 'user can go through verification uploading ID and selfie on seprerate pages' do
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect(page).not_to have_content(t('doc_auth.tips.document_capture_selfie_text1'))
+ attach_images
+ click_continue
+ expect(page).to have_title(t('doc_auth.headings.selfie_capture'))
+ expect(page).to have_content(t('doc_auth.tips.document_capture_selfie_text1'))
+ click_button 'Take photo'
+ attach_selfie
+ submit_images
+ expect(page).to have_content(t('doc_auth.headings.capture_complete'))
+ end
+
+ it 'initial verification failure allows user to resubmit all images in 1 page' do
+ attach_images(
+ Rails.root.join(
+ 'spec', 'fixtures',
+ 'ial2_test_credential_multiple_doc_auth_failures_both_sides.yml'
+ ),
+ )
+ click_continue
+ click_button 'Take photo'
+ attach_selfie(
+ Rails.root.join(
+ 'spec', 'fixtures',
+ 'ial2_test_credential_forces_error.yml'
+ ),
+ )
+ submit_images
+ expect(page).to have_content(t('doc_auth.errors.rate_limited_heading'))
+ click_try_again
+ expect(page).to have_content(t('doc_auth.headings.review_issues'))
+ attach_images
+ submit_images
+ expect(page).to have_content(t('doc_auth.headings.capture_complete'))
+ end
+ end
+
+ context 'standard desktop flow' do
+ before do
+ visit_idp_from_oidc_sp_with_ial2
+ sign_in_and_2fa_user(@user)
+ complete_doc_auth_steps_before_document_capture_step
+ end
+
+ context 'rate limits calls to backend docauth vendor', allow_browser_log: true do
+ before do
+ allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts)
+ DocAuth::Mock::DocAuthMockClient.mock_response!(
+ method: :post_front_image,
+ response: DocAuth::Response.new(
+ success: false,
+ errors: { network: I18n.t('doc_auth.errors.general.network_error') },
+ ),
+ )
+
+ (max_attempts - 1).times do
+ attach_and_submit_images
+ click_on t('idv.failure.button.warning')
+ end
+ end
+
+ it 'redirects to the rate limited error page' do
+ freeze_time do
+ attach_and_submit_images
+ timeout = distance_of_time_in_words(
+ RateLimiter.attempt_window_in_minutes(:idv_doc_auth).minutes,
+ )
+ message = strip_tags(t('doc_auth.errors.rate_limited_text_html', timeout: timeout))
+ expect(page).to have_content(message)
+ expect(page).to have_current_path(idv_session_errors_rate_limited_path)
+ end
+ end
+
+ it 'logs the rate limited analytics event for doc_auth' do
+ attach_and_submit_images
+ expect(fake_analytics).to have_logged_event(
+ 'Rate Limit Reached',
+ limiter_type: :idv_doc_auth,
+ )
+ end
+
+ context 'successfully processes image on last attempt' do
+ before { DocAuth::Mock::DocAuthMockClient.reset! }
+
+ it 'proceeds to the next page with valid info' do
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
+ attach_and_submit_images
+ expect(page).to have_current_path(idv_ssn_url)
+
+ visit idv_document_capture_path
+
+ expect(page).to have_current_path(idv_session_errors_rate_limited_path)
+ end
+ end
+ end
+
+ it 'catches network connection errors on post_front_image', allow_browser_log: true do
+ DocAuth::Mock::DocAuthMockClient.mock_response!(
+ method: :post_front_image,
+ response: DocAuth::Response.new(
+ success: false,
+ errors: { network: I18n.t('doc_auth.errors.general.network_error') },
+ ),
+ )
+
+ attach_and_submit_images
+
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error'))
+ end
+
+ it 'does not track state if state tracking is disabled' do
+ allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(false)
+ attach_and_submit_images
+
+ expect(DocAuthLog.find_by(user_id: @user.id).state).to be_nil
+ end
+ end
+
+ context 'standard mobile flow' do
+ it 'proceeds to the next page with valid info' do
+ perform_in_browser(:mobile) do
+ visit_idp_from_oidc_sp_with_ial2
+ sign_in_and_2fa_user(@user)
+ complete_doc_auth_steps_before_document_capture_step
+
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
+
+ # doc auth is successful while liveness is not req'd
+ use_id_image('ial2_test_credential_no_liveness.yml')
+ submit_images
+
+ expect(page).to have_current_path(idv_ssn_url)
+ expect_costing_for_document
+ expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY')
+
+ fill_out_ssn_form_ok
+ click_idv_continue
+ complete_verify_step
+ expect(page).to have_current_path(idv_phone_url)
+ end
+ end
+ end
context 'selfie check' do
before do
allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
@@ -159,6 +314,7 @@
fill_out_ssn_form_ok
click_idv_continue
complete_verify_step
+ # expect(page).to have_content(t('doc_auth.headings.document_capture_selfie'))
expect(page).to have_current_path(idv_phone_url)
end
end
@@ -187,10 +343,12 @@
expect(max_submission_attempts_before_native_camera.to_i).
to eq(ActiveSupport::Duration::SECONDS_PER_HOUR)
expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
- expect_doc_capture_page_header(t('doc_auth.headings.document_capture_with_selfie'))
- expect_doc_capture_id_subheader
+ expect(page).to have_text(t('doc_auth.headings.document_capture'))
+ attach_images
+ click_continue
expect_doc_capture_selfie_subheader
- attach_liveness_images
+ click_button 'Take photo'
+ attach_selfie
submit_images
expect(page).to have_current_path(idv_ssn_url)
@@ -211,7 +369,15 @@
# when there are multiple doc auth errors on front and back
it 'shows the correct error message for the given error' do
perform_in_browser(:mobile) do
+ click_continue
use_id_image('ial2_test_credential_multiple_doc_auth_failures_both_sides.yml')
+ click_continue
+ click_button 'Take photo'
+ click_idv_submit_default
+ expect(page).not_to have_content(t('doc_auth.headings.capture_complete'))
+ expect(page).not_to have_content(t('doc_auth.errors.rate_limited_heading'))
+ expect(page).to have_title(t('doc_auth.headings.selfie_capture'))
+
use_selfie_image('ial2_test_credential_multiple_doc_auth_failures_both_sides.yml')
submit_images
@@ -230,12 +396,16 @@
# Wrong doc type is uploaded
use_id_image('ial2_test_credential_wrong_doc_type.yml')
+
use_selfie_image('ial2_test_portrait_match_success.yml')
submit_images
expect_rate_limited_header(false)
expect_try_taking_new_pictures(false)
- expect_review_issues_body_message('doc_auth.errors.doc_type_not_supported_heading')
+ # eslint-disable-next-line
+ expect_review_issues_body_message(
+ 'doc_auth.errors.doc_type_not_supported_heading',
+ )
expect_review_issues_body_message('doc_auth.errors.doc.doc_type_check')
expect_rate_limit_warning(max_attempts - 2)
@@ -246,7 +416,10 @@
expect_resubmit_page_inline_selfie_error_message(false)
# when there are multiple front doc auth errors
- use_id_image('ial2_test_credential_multiple_doc_auth_failures_front_side_only.yml')
+ use_id_image(
+ 'ial2_test_credential_multiple_doc_auth_failures_front_side_only.yml',
+ )
+
use_selfie_image(
'ial2_test_credential_multiple_doc_auth_failures_front_side_only.yml',
)
@@ -262,12 +435,17 @@
expect_to_try_again
expect_resubmit_page_h1_copy
- expect_resubmit_page_body_copy('doc_auth.errors.general.multiple_front_id_failures')
+ expect_resubmit_page_body_copy(
+ 'doc_auth.errors.general.multiple_front_id_failures',
+ )
expect_resubmit_page_inline_error_messages(1)
expect_resubmit_page_inline_selfie_error_message(false)
# when there are multiple back doc auth errors
- use_id_image('ial2_test_credential_multiple_doc_auth_failures_back_side_only.yml')
+ use_id_image(
+ 'ial2_test_credential_multiple_doc_auth_failures_back_side_only.yml',
+ )
+
use_selfie_image(
'ial2_test_credential_multiple_doc_auth_failures_back_side_only.yml',
)
@@ -283,12 +461,16 @@
expect_to_try_again
expect_resubmit_page_h1_copy
- expect_resubmit_page_body_copy('doc_auth.errors.general.multiple_back_id_failures')
+ expect_resubmit_page_body_copy(
+ 'doc_auth.errors.general.multiple_back_id_failures',
+ )
expect_resubmit_page_inline_error_messages(1)
expect_resubmit_page_inline_selfie_error_message(false)
# attention barcode with invalid pii is uploaded
use_id_image('ial2_test_credential_barcode_attention_no_address.yml')
+ click_continue
+
use_selfie_image('ial2_test_portrait_match_success.yml')
submit_images
@@ -335,7 +517,6 @@
complete_up_to_how_to_verify_step_for_opt_in_ipp(
facial_match_required: true,
)
- complete_verify_step
end
end
@@ -430,10 +611,12 @@
click_on t('forms.buttons.upload_photos')
expect(page).to have_current_path(idv_document_capture_url)
expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
- expect_doc_capture_page_header(t('doc_auth.headings.document_capture_with_selfie'))
- expect_doc_capture_id_subheader
+ expect(page).to have_text(t('doc_auth.headings.document_capture'))
+ attach_images
+ click_continue
expect_doc_capture_selfie_subheader
- attach_liveness_images
+ click_button 'Take photo'
+ attach_selfie
submit_images
expect(page).to have_current_path(idv_ssn_url)
@@ -465,7 +648,7 @@
visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
sign_in_and_2fa_user(@user)
complete_doc_auth_steps_before_hybrid_handoff_step
- # we still have option to continue on handoff, since it's desktop no skip_hand_off
+ # still have option to continue handoff, since it's desktop no skip_hand_off
expect(page).to have_current_path(idv_hybrid_handoff_path)
expect(page).to have_content(t('doc_auth.headings.hybrid_handoff_selfie'))
click_on t('in_person_proofing.headings.prepare')
@@ -485,533 +668,6 @@
end
end
- context 'split doc auth flow', allow_browser_log: true do
- before do
- allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
- allow(IdentityConfig.store).to receive(:doc_auth_selfie_capture_enabled).and_return(true)
- allow(IdentityConfig.store).to receive(:doc_auth_separate_pages_enabled).and_return(true)
- visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
- sign_in_and_2fa_user(@user)
- complete_doc_auth_steps_before_document_capture_step
- end
- it 'user can go through verification uploading ID and selfie on seprerate pages' do
- expect(page).to have_current_path(idv_document_capture_url)
- expect(page).not_to have_content(t('doc_auth.tips.document_capture_selfie_text1'))
- attach_images
- click_continue
- expect(page).to have_title(t('doc_auth.headings.selfie_capture'))
- expect(page).to have_content(t('doc_auth.tips.document_capture_selfie_text1'))
- attach_selfie
- submit_images
- expect(page).to have_content(t('doc_auth.headings.capture_complete'))
- end
- it 'initial verification failure allows user to resubmit all images in 1 page' do
- attach_images(
- Rails.root.join(
- 'spec', 'fixtures',
- 'ial2_test_credential_multiple_doc_auth_failures_both_sides.yml'
- ),
- )
- click_continue
- attach_selfie(
- Rails.root.join(
- 'spec', 'fixtures',
- 'ial2_test_credential_forces_error.yml'
- ),
- )
- submit_images
- expect(page).to have_content(t('doc_auth.errors.rate_limited_heading'))
- click_try_again
- expect(page).to have_content(t('doc_auth.headings.review_issues'))
- attach_images
- attach_selfie
- submit_images
- expect(page).to have_content(t('doc_auth.headings.capture_complete'))
- end
- context 'standard desktop flow' do
- before do
- visit_idp_from_oidc_sp_with_ial2
- sign_in_and_2fa_user(@user)
- complete_doc_auth_steps_before_document_capture_step
- end
-
- context 'rate limits calls to backend docauth vendor', allow_browser_log: true do
- before do
- allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts)
- DocAuth::Mock::DocAuthMockClient.mock_response!(
- method: :post_front_image,
- response: DocAuth::Response.new(
- success: false,
- errors: { network: I18n.t('doc_auth.errors.general.network_error') },
- ),
- )
-
- (max_attempts - 1).times do
- attach_and_submit_images
- click_on t('idv.failure.button.warning')
- end
- end
-
- it 'redirects to the rate limited error page' do
- freeze_time do
- attach_and_submit_images
- timeout = distance_of_time_in_words(
- RateLimiter.attempt_window_in_minutes(:idv_doc_auth).minutes,
- )
- message = strip_tags(t('doc_auth.errors.rate_limited_text_html', timeout: timeout))
- expect(page).to have_content(message)
- expect(page).to have_current_path(idv_session_errors_rate_limited_path)
- end
- end
-
- it 'logs the rate limited analytics event for doc_auth' do
- attach_and_submit_images
- expect(fake_analytics).to have_logged_event(
- 'Rate Limit Reached',
- limiter_type: :idv_doc_auth,
- )
- end
-
- context 'successfully processes image on last attempt' do
- before { DocAuth::Mock::DocAuthMockClient.reset! }
-
- it 'proceeds to the next page with valid info' do
- expect(page).to have_current_path(idv_document_capture_url)
- expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
- attach_and_submit_images
- expect(page).to have_current_path(idv_ssn_url)
-
- visit idv_document_capture_path
-
- expect(page).to have_current_path(idv_session_errors_rate_limited_path)
- end
- end
- end
-
- it 'catches network connection errors on post_front_image', allow_browser_log: true do
- DocAuth::Mock::DocAuthMockClient.mock_response!(
- method: :post_front_image,
- response: DocAuth::Response.new(
- success: false,
- errors: { network: I18n.t('doc_auth.errors.general.network_error') },
- ),
- )
-
- attach_and_submit_images
-
- expect(page).to have_current_path(idv_document_capture_url)
- expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error'))
- end
-
- it 'does not track state if state tracking is disabled' do
- allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(false)
- attach_and_submit_images
-
- expect(DocAuthLog.find_by(user_id: @user.id).state).to be_nil
- end
- end
-
- context 'standard mobile flow' do
- it 'proceeds to the next page with valid info' do
- perform_in_browser(:mobile) do
- visit_idp_from_oidc_sp_with_ial2
- sign_in_and_2fa_user(@user)
- complete_doc_auth_steps_before_document_capture_step
-
- expect(page).to have_current_path(idv_document_capture_url)
- expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
- expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
-
- # doc auth is successful while liveness is not req'd
- use_id_image('ial2_test_credential_no_liveness.yml')
- submit_images
-
- expect(page).to have_current_path(idv_ssn_url)
- expect_costing_for_document
- expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY')
-
- fill_out_ssn_form_ok
- click_idv_continue
- complete_verify_step
- expect(page).to have_current_path(idv_phone_url)
- end
- end
- end
- context 'selfie check' do
- before do
- allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
- end
-
- context 'when a selfie is not requested by SP' do
- it 'proceeds to the next page with valid info, excluding a selfie image' do
- perform_in_browser(:mobile) do
- visit_idp_from_oidc_sp_with_ial2
- sign_in_and_2fa_user(@user)
- complete_doc_auth_steps_before_document_capture_step
-
- expect(page).to have_current_path(idv_document_capture_url)
- expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
-
- expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
-
- attach_images
- submit_images
-
- expect(page).to have_current_path(idv_ssn_url)
- expect_costing_for_document
- expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('MT')
-
- expect(page).to have_current_path(idv_ssn_url)
- fill_out_ssn_form_ok
- click_idv_continue
- complete_verify_step
- # expect(page).to have_content(t('doc_auth.headings.document_capture_selfie'))
- expect(page).to have_current_path(idv_phone_url)
- end
- end
- end
-
- context 'when a selfie is required by the SP' do
- context 'on mobile platform', allow_browser_log: true do
- before do
- # mock mobile device as cameraCapable, this allows us to process
- allow_any_instance_of(ActionController::Parameters).
- to receive(:[]).and_wrap_original do |impl, param_name|
- param_name.to_sym == :skip_hybrid_handoff ? '' : impl.call(param_name)
- end
- end
-
- context 'with a passing selfie' do
- it 'proceeds to the next page with valid info, including a selfie image' do
- perform_in_browser(:mobile) do
- visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
- sign_in_and_2fa_user(@user)
- complete_doc_auth_steps_before_document_capture_step
-
- expect(page).to have_current_path(idv_document_capture_url)
- expect(max_capture_attempts_before_native_camera.to_i).
- to eq(ActiveSupport::Duration::SECONDS_PER_HOUR)
- expect(max_submission_attempts_before_native_camera.to_i).
- to eq(ActiveSupport::Duration::SECONDS_PER_HOUR)
- expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
- expect(page).to have_text(t('doc_auth.headings.document_capture'))
- attach_images
- click_continue
- expect_doc_capture_selfie_subheader
- attach_selfie
- submit_images
-
- expect(page).to have_current_path(idv_ssn_url)
- expect_costing_for_document
- expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('MT')
-
- expect(page).to have_current_path(idv_ssn_url)
- fill_out_ssn_form_ok
- click_idv_continue
- complete_verify_step
- expect(page).to have_current_path(idv_phone_url)
- end
- end
- end
-
- context 'documents or selfie with error is uploaded' do
- shared_examples 'it has correct error displays' do
- # when there are multiple doc auth errors on front and back
- it 'shows the correct error message for the given error' do
- perform_in_browser(:mobile) do
- click_continue
- use_id_image('ial2_test_credential_multiple_doc_auth_failures_both_sides.yml')
- click_continue
- click_idv_submit_default
- expect(page).not_to have_content(t('doc_auth.headings.capture_complete'))
- expect(page).not_to have_content(t('doc_auth.errors.rate_limited_heading'))
- expect(page).to have_title(t('doc_auth.headings.selfie_capture'))
-
- use_selfie_image('ial2_test_credential_multiple_doc_auth_failures_both_sides.yml')
- submit_images
-
- expect_rate_limited_header(true)
-
- expect_try_taking_new_pictures
- expect_review_issues_body_message('doc_auth.errors.general.no_liveness')
- expect_rate_limit_warning(max_attempts - 1)
-
- expect_to_try_again
- expect_resubmit_page_h1_copy
-
- expect_resubmit_page_body_copy('doc_auth.errors.general.no_liveness')
- expect_resubmit_page_inline_error_messages(2)
- expect_resubmit_page_inline_selfie_error_message(false)
-
- # Wrong doc type is uploaded
- use_id_image('ial2_test_credential_wrong_doc_type.yml')
- use_selfie_image('ial2_test_portrait_match_success.yml')
- submit_images
-
- expect_rate_limited_header(false)
- expect_try_taking_new_pictures(false)
- # eslint-disable-next-line
- expect_review_issues_body_message(
- 'doc_auth.errors.doc_type_not_supported_heading',
- )
- expect_review_issues_body_message('doc_auth.errors.doc.doc_type_check')
- expect_rate_limit_warning(max_attempts - 2)
-
- expect_to_try_again
- expect_resubmit_page_h1_copy
-
- expect_review_issues_body_message('doc_auth.errors.card_type')
- expect_resubmit_page_inline_selfie_error_message(false)
-
- # when there are multiple front doc auth errors
- use_id_image(
- 'ial2_test_credential_multiple_doc_auth_failures_front_side_only.yml',
- )
- use_selfie_image(
- 'ial2_test_credential_multiple_doc_auth_failures_front_side_only.yml',
- )
- submit_images
-
- expect_rate_limited_header(true)
- expect_try_taking_new_pictures(false)
- expect_review_issues_body_message(
- 'doc_auth.errors.general.multiple_front_id_failures',
- )
- expect_rate_limit_warning(max_attempts - 3)
-
- expect_to_try_again
- expect_resubmit_page_h1_copy
-
- expect_resubmit_page_body_copy(
- 'doc_auth.errors.general.multiple_front_id_failures',
- )
- expect_resubmit_page_inline_error_messages(1)
- expect_resubmit_page_inline_selfie_error_message(false)
-
- # when there are multiple back doc auth errors
- use_id_image(
- 'ial2_test_credential_multiple_doc_auth_failures_back_side_only.yml',
- )
- use_selfie_image(
- 'ial2_test_credential_multiple_doc_auth_failures_back_side_only.yml',
- )
- submit_images
-
- expect_rate_limited_header(true)
- expect_try_taking_new_pictures(false)
- expect_review_issues_body_message(
- 'doc_auth.errors.general.multiple_back_id_failures',
- )
- expect_rate_limit_warning(max_attempts - 4)
-
- expect_to_try_again
- expect_resubmit_page_h1_copy
-
- expect_resubmit_page_body_copy(
- 'doc_auth.errors.general.multiple_back_id_failures',
- )
- expect_resubmit_page_inline_error_messages(1)
- expect_resubmit_page_inline_selfie_error_message(false)
-
- # attention barcode with invalid pii is uploaded
- use_id_image('ial2_test_credential_barcode_attention_no_address.yml')
- use_selfie_image('ial2_test_portrait_match_success.yml')
- submit_images
-
- expect(page).to have_content(t('doc_auth.errors.alerts.address_check'))
- expect(page).to have_current_path(idv_document_capture_path)
-
- click_try_again
-
- # And finally, after lots of errors, we can still succeed
- attach_images
- submit_images
-
- expect(page).to have_current_path(idv_ssn_path)
- end
- end
- end
-
- context 'IPP enabled' do
- let(:ipp_service_provider) do
- create(:service_provider, :active, :in_person_proofing_enabled)
- end
-
- 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_any_instance_of(ServiceProvider).to receive(
- :in_person_proofing_enabled,
- ).and_return(true)
- allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(99)
- 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])
- perform_in_browser(:mobile) do
- visit_idp_from_sp_with_ial2(
- :oidc,
- **{ client_id: ipp_service_provider.issuer,
- facial_match_required: true },
- )
- sign_in_and_2fa_user(@user)
- complete_up_to_how_to_verify_step_for_opt_in_ipp(
- facial_match_required: true,
- )
- end
- end
-
- it_should_behave_like 'it has correct error displays'
- end
-
- context 'IPP not enabled' do
- before do
- allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(99)
- perform_in_browser(:mobile) do
- visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
- sign_in_and_2fa_user(@user)
- complete_doc_auth_steps_before_document_capture_step
- end
- end
-
- it_should_behave_like 'it has correct error displays'
- end
- end
-
- context 'when selfie check is not enabled (flag off, and/or in production)' do
- it 'proceeds to the next page with valid info, excluding a selfie image' do
- perform_in_browser(:mobile) do
- visit_idp_from_oidc_sp_with_ial2
- sign_in_and_2fa_user(@user)
- complete_doc_auth_steps_before_document_capture_step
-
- expect(page).to have_current_path(idv_document_capture_url)
- expect(max_capture_attempts_before_native_camera).to eq(
- IdentityConfig.store.doc_auth_max_capture_attempts_before_native_camera.to_s,
- )
- expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
-
- expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
-
- expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
- attach_images
- submit_images
-
- expect(page).to have_current_path(idv_ssn_url)
- expect_costing_for_document
- expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('MT')
-
- expect(page).to have_current_path(idv_ssn_url)
- fill_out_ssn_form_ok
- click_idv_continue
- complete_verify_step
- expect(page).to have_current_path(idv_phone_url)
- end
- end
- end
- end
-
- context 'on desktop' do
- let(:desktop_selfie_mode) { false }
-
- before do
- allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode).
- and_return(desktop_selfie_mode)
- end
-
- describe 'when desktop selfie not allowed' do
- it 'can only proceed to link sent page' do
- perform_in_browser(:desktop) do
- visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
- sign_in_and_2fa_user(@user)
- complete_doc_auth_steps_before_hybrid_handoff_step
- # we still have option to continue
- expect(page).to have_current_path(idv_hybrid_handoff_path)
- expect(page).to have_content(t('doc_auth.headings.hybrid_handoff_selfie'))
- expect(page).not_to have_content(t('doc_auth.headings.hybrid_handoff'))
- expect(page).not_to have_content(t('doc_auth.info.upload_from_computer'))
- click_on t('forms.buttons.send_link')
- expect(page).to have_current_path(idv_link_sent_path)
- end
- end
- end
-
- describe 'when desktop selfie is allowed' do
- let(:desktop_selfie_mode) { true }
-
- it 'proceed to the next page with valid info, including a selfie image' do
- perform_in_browser(:desktop) do
- visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
- sign_in_and_2fa_user(@user)
- complete_doc_auth_steps_before_hybrid_handoff_step
- # we still have option to continue on handoff, since it's desktop no skip_hand_off
- expect(page).to have_current_path(idv_hybrid_handoff_path)
- expect(page).to have_content(t('doc_auth.headings.hybrid_handoff_selfie'))
- expect(page).not_to have_content(t('doc_auth.headings.hybrid_handoff'))
- expect(page).to have_content(t('doc_auth.info.upload_from_computer'))
- click_on t('forms.buttons.upload_photos')
- expect(page).to have_current_path(idv_document_capture_url)
- expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
- expect(page).to have_text(t('doc_auth.headings.document_capture'))
- attach_images
- click_continue
- expect_doc_capture_selfie_subheader
- attach_selfie
- submit_images
-
- expect(page).to have_current_path(idv_ssn_url)
- expect_costing_for_document
- expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('MT')
-
- expect(page).to have_current_path(idv_ssn_url)
- fill_out_ssn_form_ok
- click_idv_continue
- complete_verify_step
- expect(page).to have_current_path(idv_phone_url)
- end
- end
-
- context 'when ipp is enabled' do
- let(:in_person_doc_auth_button_enabled) { true }
- let(:sp_ipp_enabled) { true }
-
- before do
- allow(IdentityConfig.store).to receive(:in_person_doc_auth_button_enabled).
- and_return(in_person_doc_auth_button_enabled)
- allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).with(anything).
- and_return(sp_ipp_enabled)
- end
-
- describe 'when ipp is selected' do
- it 'proceed to the next page and start ipp' do
- perform_in_browser(:desktop) do
- visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
- sign_in_and_2fa_user(@user)
- complete_doc_auth_steps_before_hybrid_handoff_step
- # still have option to continue handoff, since it's desktop no skip_hand_off
- expect(page).to have_current_path(idv_hybrid_handoff_path)
- expect(page).to have_content(t('doc_auth.headings.hybrid_handoff_selfie'))
- click_on t('in_person_proofing.headings.prepare')
- expect(page).to have_current_path(
- idv_document_capture_path({ step: 'hybrid_handoff' }),
- )
- expect_step_indicator_current_step(
- t('step_indicator.flows.idv.find_a_post_office'),
- )
- expect_doc_capture_page_header(t('in_person_proofing.headings.prepare'))
- end
- end
- end
- end
- end
- end
- end
- end
- end
-
def expect_rate_limited_header(expected_to_be_present)
review_issues_h1_heading = strip_tags(t('doc_auth.errors.rate_limited_heading'))
if expected_to_be_present
diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb
index 71010891857..437db0408f3 100644
--- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb
+++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb
@@ -11,248 +11,244 @@
IdentityConfig.store.idv_send_link_attempt_window_in_minutes
end
let(:facial_match_required) { false }
- context 'split doc auth', allow_browser_log: true do
+ before do
+ if facial_match_required
+ visit_idp_from_oidc_sp_with_ial2(
+ facial_match_required: facial_match_required,
+ )
+ end
+ sign_in_and_2fa_user
+ allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics)
+ end
+ context 'on a desktop device send link' do
before do
- allow(IdentityConfig.store).to receive(:doc_auth_separate_pages_enabled).and_return(true)
- if facial_match_required
- visit_idp_from_oidc_sp_with_ial2(
- facial_match_required: facial_match_required,
- )
- end
- sign_in_and_2fa_user
- allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics)
+ complete_doc_auth_steps_before_hybrid_handoff_step
end
- context 'on a desktop device send link' do
- before do
- complete_doc_auth_steps_before_hybrid_handoff_step
- end
-
- it 'has the forms with the expected aria attributes' do
- mobile_form = find('#form-to-submit-photos-through-mobile')
- desktop_form = find('#form-to-submit-photos-through-desktop')
+ it 'has the forms with the expected aria attributes' do
+ mobile_form = find('#form-to-submit-photos-through-mobile')
+ desktop_form = find('#form-to-submit-photos-through-desktop')
- expect(mobile_form).to have_name(t('forms.buttons.send_link'))
- expect(desktop_form).to have_name(t('forms.buttons.upload_photos'))
- end
-
- it 'proceeds to link sent page when user chooses to use phone' do
- click_send_link
+ expect(mobile_form).to have_name(t('forms.buttons.send_link'))
+ expect(desktop_form).to have_name(t('forms.buttons.upload_photos'))
+ end
- expect(page).to have_current_path(idv_link_sent_path)
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth hybrid handoff submitted',
- hash_including(step: 'hybrid_handoff', destination: :link_sent),
- )
- end
+ it 'proceeds to link sent page when user chooses to use phone' do
+ click_send_link
- it 'proceeds to the next page with valid info', :js do
- expect(Telephony).to receive(:send_doc_auth_link).
- with(hash_including(to: '+1 415-555-0199')).
- and_call_original
+ expect(page).to have_current_path(idv_link_sent_path)
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth hybrid handoff submitted',
+ hash_including(step: 'hybrid_handoff', destination: :link_sent),
+ )
+ end
- expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
+ it 'proceeds to the next page with valid info', :js do
+ expect(Telephony).to receive(:send_doc_auth_link).
+ with(hash_including(to: '+1 415-555-0199')).
+ and_call_original
- fill_in :doc_auth_phone, with: '415-555-0199'
- click_send_link
-
- expect(page).to have_current_path(idv_link_sent_path)
- end
+ expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
- it 'does not proceed to the next page with invalid info', :js do
- fill_in :doc_auth_phone, with: ''
- click_send_link
+ fill_in :doc_auth_phone, with: '415-555-0199'
+ click_send_link
- expect(page).to have_current_path(idv_hybrid_handoff_path, ignore_query: true)
- end
+ expect(page).to have_current_path(idv_link_sent_path)
+ end
- it 'sends a link that does not contain any underscores' do
- # because URLs with underscores sometimes get messed up by carriers
- expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config|
- expect(config[:link]).to_not include('_')
+ it 'does not proceed to the next page with invalid info', :js do
+ fill_in :doc_auth_phone, with: ''
+ click_send_link
- impl.call(**config)
- end
+ expect(page).to have_current_path(idv_hybrid_handoff_path, ignore_query: true)
+ end
- fill_in :doc_auth_phone, with: '415-555-0199'
- click_send_link
+ it 'sends a link that does not contain any underscores' do
+ # because URLs with underscores sometimes get messed up by carriers
+ expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config|
+ expect(config[:link]).to_not include('_')
- expect(page).to have_current_path(idv_link_sent_path)
+ impl.call(**config)
end
- it 'does not proceed if Telephony raises an error' do
- fill_in :doc_auth_phone, with: '225-555-1000'
+ fill_in :doc_auth_phone, with: '415-555-0199'
+ click_send_link
- click_send_link
+ expect(page).to have_current_path(idv_link_sent_path)
+ end
- expect(page).to have_current_path(idv_hybrid_handoff_path, ignore_query: true)
- expect(page).to have_content I18n.t('telephony.error.friendly_message.generic')
- end
+ it 'does not proceed if Telephony raises an error' do
+ fill_in :doc_auth_phone, with: '225-555-1000'
- it 'displays error if user selects a country to which we cannot send SMS', js: true do
- click_on t('components.phone_input.country_code_label')
- within(page.find('.iti__country-container', visible: :all)) do
- find('span', text: 'Sri Lanka').click
- end
- focused_input = page.find('.phone-input__number:focus')
+ click_send_link
- error_message_id = focused_input[:'aria-describedby']&.split(' ')&.find do |id|
- page.has_css?(".usa-error-message##{id}")
- end
- expect(error_message_id).to_not be_empty
+ expect(page).to have_current_path(idv_hybrid_handoff_path, ignore_query: true)
+ expect(page).to have_content I18n.t('telephony.error.friendly_message.generic')
+ end
- error_message = page.find_by_id(error_message_id)
- expect(error_message).to have_content(
- t(
- 'two_factor_authentication.otp_delivery_preference.sms_unsupported',
- location: 'Sri Lanka',
- ),
- )
- click_send_link
- expect(page.find(':focus')).to match_css('.phone-input__number')
+ it 'displays error if user selects a country to which we cannot send SMS', js: true do
+ click_on t('components.phone_input.country_code_label')
+ within(page.find('.iti__country-container', visible: :all)) do
+ find('span', text: 'Sri Lanka').click
end
+ focused_input = page.find('.phone-input__number:focus')
- it 'rate limits sending the link' do
- user = user_with_2fa
- sign_in_and_2fa_user(user)
- complete_doc_auth_steps_before_hybrid_handoff_step
- timeout = distance_of_time_in_words(
- RateLimiter.attempt_window_in_minutes(:idv_send_link).minutes,
- )
- allow(IdentityConfig.store).to receive(:idv_send_link_max_attempts).
- and_return(idv_send_link_max_attempts)
-
- freeze_time do
- idv_send_link_max_attempts.times do
- expect(page).to_not have_content(
- I18n.t('doc_auth.errors.send_link_limited', timeout: timeout),
- )
-
- fill_in :doc_auth_phone, with: '415-555-0199'
- click_send_link
-
- expect(page).to have_current_path(idv_link_sent_path)
-
- click_doc_auth_back_link
- end
+ error_message_id = focused_input[:'aria-describedby']&.split(' ')&.find do |id|
+ page.has_css?(".usa-error-message##{id}")
+ end
+ expect(error_message_id).to_not be_empty
+
+ error_message = page.find_by_id(error_message_id)
+ expect(error_message).to have_content(
+ t(
+ 'two_factor_authentication.otp_delivery_preference.sms_unsupported',
+ location: 'Sri Lanka',
+ ),
+ )
+ click_send_link
+ expect(page.find(':focus')).to match_css('.phone-input__number')
+ end
- fill_in :doc_auth_phone, with: '415-555-0199'
+ it 'rate limits sending the link' do
+ user = user_with_2fa
+ sign_in_and_2fa_user(user)
+ complete_doc_auth_steps_before_hybrid_handoff_step
+ timeout = distance_of_time_in_words(
+ RateLimiter.attempt_window_in_minutes(:idv_send_link).minutes,
+ )
+ allow(IdentityConfig.store).to receive(:idv_send_link_max_attempts).
+ and_return(idv_send_link_max_attempts)
- click_send_link
- expect(page).to have_current_path(idv_hybrid_handoff_path, ignore_query: true)
- expect(page).to have_content(
- I18n.t(
- 'doc_auth.errors.send_link_limited',
- timeout: timeout,
- ),
+ freeze_time do
+ idv_send_link_max_attempts.times do
+ expect(page).to_not have_content(
+ I18n.t('doc_auth.errors.send_link_limited', timeout: timeout),
)
- expect(page).to have_selector('h1', text: t('doc_auth.headings.hybrid_handoff'))
- expect(page).to have_selector('h2', text: t('doc_auth.headings.upload_from_phone'))
- end
- expect(fake_analytics).to have_logged_event(
- 'Rate Limit Reached',
- limiter_type: :idv_send_link,
- )
- # Manual expiration is needed for now since the RateLimiter uses
- # Redis ttl instead of expiretime
- RateLimiter.new(rate_limit_type: :idv_send_link, user: user).reset!
- travel_to(Time.zone.now + idv_send_link_attempt_window_in_minutes.minutes) do
fill_in :doc_auth_phone, with: '415-555-0199'
click_send_link
- expect(page).to have_current_path(idv_link_sent_path)
- end
- end
- it 'includes expected URL parameters' do
- expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config|
- params = Rack::Utils.parse_nested_query URI(config[:link]).query
-
- expect(params['document-capture-session']).to be_a_kind_of(String)
+ expect(page).to have_current_path(idv_link_sent_path)
- impl.call(**config)
+ click_doc_auth_back_link
end
fill_in :doc_auth_phone, with: '415-555-0199'
+
click_send_link
+ expect(page).to have_current_path(idv_hybrid_handoff_path, ignore_query: true)
+ expect(page).to have_content(
+ I18n.t(
+ 'doc_auth.errors.send_link_limited',
+ timeout: timeout,
+ ),
+ )
+ expect(page).to have_selector('h1', text: t('doc_auth.headings.hybrid_handoff'))
+ expect(page).to have_selector('h2', text: t('doc_auth.headings.upload_from_phone'))
end
+ expect(fake_analytics).to have_logged_event(
+ 'Rate Limit Reached',
+ limiter_type: :idv_send_link,
+ )
- it 'sets requested_at on the capture session' do
- doc_capture_session_uuid = nil
-
- expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config|
- params = Rack::Utils.parse_nested_query URI(config[:link]).query
- doc_capture_session_uuid = params['document-capture-session']
- impl.call(**config)
- end
-
+ # Manual expiration is needed for now since the RateLimiter uses
+ # Redis ttl instead of expiretime
+ RateLimiter.new(rate_limit_type: :idv_send_link, user: user).reset!
+ travel_to(Time.zone.now + idv_send_link_attempt_window_in_minutes.minutes) do
fill_in :doc_auth_phone, with: '415-555-0199'
click_send_link
+ expect(page).to have_current_path(idv_link_sent_path)
+ end
+ end
+
+ it 'includes expected URL parameters' do
+ expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config|
+ params = Rack::Utils.parse_nested_query URI(config[:link]).query
+
+ expect(params['document-capture-session']).to be_a_kind_of(String)
- document_capture_session = DocumentCaptureSession.find_by(uuid: doc_capture_session_uuid)
- expect(document_capture_session).to be
- expect(document_capture_session).to have_attributes(requested_at: a_kind_of(Time))
+ impl.call(**config)
end
+
+ fill_in :doc_auth_phone, with: '415-555-0199'
+ click_send_link
end
- context 'on a desktop device and selfie is allowed' do
- before do
- complete_doc_auth_steps_before_hybrid_handoff_step
+ it 'sets requested_at on the capture session' do
+ doc_capture_session_uuid = nil
+
+ expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config|
+ params = Rack::Utils.parse_nested_query URI(config[:link]).query
+ doc_capture_session_uuid = params['document-capture-session']
+ impl.call(**config)
end
- describe 'when selfie is required by sp' do
- let(:facial_match_required) { true }
- it 'has expected UI elements' do
- mobile_form = find('#form-to-submit-photos-through-mobile')
- expect(mobile_form).to have_name(t('forms.buttons.send_link'))
- expect(page).to have_selector('h1', text: t('doc_auth.headings.hybrid_handoff_selfie'))
+ fill_in :doc_auth_phone, with: '415-555-0199'
+ click_send_link
+
+ document_capture_session = DocumentCaptureSession.find_by(uuid: doc_capture_session_uuid)
+ expect(document_capture_session).to be
+ expect(document_capture_session).to have_attributes(requested_at: a_kind_of(Time))
+ end
+ end
+
+ context 'on a desktop device and selfie is allowed' do
+ before do
+ complete_doc_auth_steps_before_hybrid_handoff_step
+ end
+
+ describe 'when selfie is required by sp' do
+ let(:facial_match_required) { true }
+ it 'has expected UI elements' do
+ mobile_form = find('#form-to-submit-photos-through-mobile')
+ expect(mobile_form).to have_name(t('forms.buttons.send_link'))
+ expect(page).to have_selector('h1', text: t('doc_auth.headings.hybrid_handoff_selfie'))
+ end
+ context 'on a desktop choose ipp', js: true do
+ let(:in_person_doc_auth_button_enabled) { true }
+ let(:sp_ipp_enabled) { true }
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_doc_auth_button_enabled).
+ and_return(in_person_doc_auth_button_enabled)
+ allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).with(anything).
+ and_return(sp_ipp_enabled)
+ complete_doc_auth_steps_before_hybrid_handoff_step
end
- context 'on a desktop choose ipp', js: true do
- let(:in_person_doc_auth_button_enabled) { true }
- let(:sp_ipp_enabled) { true }
- before do
- allow(IdentityConfig.store).to receive(:in_person_doc_auth_button_enabled).
- and_return(in_person_doc_auth_button_enabled)
- allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).with(anything).
- and_return(sp_ipp_enabled)
- complete_doc_auth_steps_before_hybrid_handoff_step
- end
- context 'when ipp is enabled' do
- it 'proceeds to ipp if selected and can go back' do
- expect(page).to have_content(strip_tags(t('doc_auth.info.hybrid_handoff_ipp_html')))
- click_on t('in_person_proofing.headings.prepare')
- hybrid_step = { step: 'hybrid_handoff' }
- expect(page).to have_current_path(idv_document_capture_path(hybrid_step))
- click_on t('forms.buttons.back')
- expect(page).to have_current_path(idv_hybrid_handoff_path)
- end
+ context 'when ipp is enabled' do
+ it 'proceeds to ipp if selected and can go back' do
+ expect(page).to have_content(strip_tags(t('doc_auth.info.hybrid_handoff_ipp_html')))
+ click_on t('in_person_proofing.headings.prepare')
+ hybrid_step = { step: 'hybrid_handoff' }
+ expect(page).to have_current_path(idv_document_capture_path(hybrid_step))
+ click_on t('forms.buttons.back')
+ expect(page).to have_current_path(idv_hybrid_handoff_path)
end
+ end
- context 'when ipp is disabled' do
- let(:in_person_doc_auth_button_enabled) { false }
- let(:sp_ipp_enabled) { false }
- it 'has no ipp option can be selected' do
- expect(page).to_not have_content(
- strip_tags(t('doc_auth.info.hybrid_handoff_ipp_html')),
- )
- expect(page).to_not have_content(
- t('in_person_proofing.headings.prepare'),
- )
- end
+ context 'when ipp is disabled' do
+ let(:in_person_doc_auth_button_enabled) { false }
+ let(:sp_ipp_enabled) { false }
+ it 'has no ipp option can be selected' do
+ expect(page).to_not have_content(
+ strip_tags(t('doc_auth.info.hybrid_handoff_ipp_html')),
+ )
+ expect(page).to_not have_content(
+ t('in_person_proofing.headings.prepare'),
+ )
end
end
end
+ end
- describe 'when selfie is not required by sp' do
- let(:facial_match_required) { false }
- it 'has expected UI elements' do
- mobile_form = find('#form-to-submit-photos-through-mobile')
- desktop_form = find('#form-to-submit-photos-through-desktop')
+ describe 'when selfie is not required by sp' do
+ let(:facial_match_required) { false }
+ it 'has expected UI elements' do
+ mobile_form = find('#form-to-submit-photos-through-mobile')
+ desktop_form = find('#form-to-submit-photos-through-desktop')
- expect(mobile_form).to have_name(t('forms.buttons.send_link'))
- expect(desktop_form).to have_name(t('forms.buttons.upload_photos'))
- end
+ expect(mobile_form).to have_name(t('forms.buttons.send_link'))
+ expect(desktop_form).to have_name(t('forms.buttons.upload_photos'))
end
end
end
diff --git a/spec/features/idv/doc_auth/redo_document_capture_spec.rb b/spec/features/idv/doc_auth/redo_document_capture_spec.rb
index a80aeaab178..1f960747a40 100644
--- a/spec/features/idv/doc_auth/redo_document_capture_spec.rb
+++ b/spec/features/idv/doc_auth/redo_document_capture_spec.rb
@@ -1,838 +1,805 @@
require 'rails_helper'
-RSpec.feature 'doc auth redo document capture', js: true, allowed_extra_analytics: [:*] do
+RSpec.feature 'document capture step', :js do
include IdvStepHelper
include DocAuthHelper
include DocCaptureHelper
+ include ActionView::Helpers::DateHelper
+ let(:max_attempts) { IdentityConfig.store.doc_auth_max_attempts }
let(:fake_analytics) { FakeAnalytics.new }
- before do
+ before(:each) do
allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics)
+ allow_any_instance_of(ServiceProviderSession).to receive(:sp_name).and_return(@sp_name)
end
- context 'when barcode scan returns a warning', allow_browser_log: true do
- let(:use_bad_ssn) { false }
+ before(:all) do
+ @sp_name = 'Test SP'
+ @user = user_with_2fa
+ end
+
+ after(:all) { @user.destroy }
+ context 'standard desktop flow' do
before do
- sign_in_and_2fa_user
+ visit_idp_from_oidc_sp_with_ial2
+ sign_in_and_2fa_user(@user)
complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_attention_with_barcode
- attach_and_submit_images
- click_idv_continue
-
- if use_bad_ssn
- fill_out_ssn_form_with_ssn_that_fails_resolution
- else
- fill_out_ssn_form_ok
- end
-
- click_idv_continue
end
- it 'shows a warning message to allow the user to return to upload new images' do
- warning_link_text = t('doc_auth.headings.capture_scan_warning_link')
-
- expect(page).to have_css(
- '[role="status"]',
- text: strip_nbsp(
- t(
- 'doc_auth.headings.capture_scan_warning_html',
- link_html: warning_link_text,
+ context 'rate limits calls to backend docauth vendor', allow_browser_log: true do
+ before do
+ allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts)
+ DocAuth::Mock::DocAuthMockClient.mock_response!(
+ method: :post_front_image,
+ response: DocAuth::Response.new(
+ success: false,
+ errors: { network: I18n.t('doc_auth.errors.general.network_error') },
),
- ),
- )
- click_link warning_link_text
-
- expect(current_path).to eq(idv_hybrid_handoff_path)
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth hybrid handoff visited',
- hash_including(redo_document_capture: true),
- )
- complete_hybrid_handoff_step
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth document_capture visited',
- hash_including(redo_document_capture: true),
- )
- DocAuth::Mock::DocAuthMockClient.reset!
- attach_and_submit_images
-
- expect(current_path).to eq(idv_ssn_path)
- expect(page).to have_css('[role="status"]') # We verified your ID
- complete_ssn_step
-
- expect(current_path).to eq(idv_verify_info_path)
- check t('forms.ssn.show')
- expect(page).to have_content(DocAuthHelper::GOOD_SSN)
- end
-
- context 'with a bad SSN' do
- let(:use_bad_ssn) { true }
-
- it 'shows a troubleshooting option to allow the user to cancel and return to SP' do
- complete_verify_step
- expect(page).to have_link(
- t('links.cancel'),
- href: idv_cancel_path(step: :invalid_session),
)
- click_link t('links.cancel')
-
- expect(current_path).to eq(idv_cancel_path)
+ (max_attempts - 1).times do
+ attach_and_submit_images
+ click_on t('idv.failure.button.warning')
+ end
end
- end
- context 'on mobile', driver: :headless_chrome_mobile do
- it 'shows a warning message to allow the user to return to upload new images' do
- warning_link_text = t('doc_auth.headings.capture_scan_warning_link')
-
- expect(page).to have_css(
- '[role="status"]',
- text: strip_nbsp(
- t(
- 'doc_auth.headings.capture_scan_warning_html',
- link_html: warning_link_text,
- ),
- ),
- )
- click_link warning_link_text
+ it 'redirects to the rate limited error page' do
+ freeze_time do
+ attach_and_submit_images
+ timeout = distance_of_time_in_words(
+ RateLimiter.attempt_window_in_minutes(:idv_doc_auth).minutes,
+ )
+ message = strip_tags(t('doc_auth.errors.rate_limited_text_html', timeout: timeout))
+ expect(page).to have_content(message)
+ expect(page).to have_current_path(idv_session_errors_rate_limited_path)
+ end
+ end
- expect(current_path).to eq(idv_document_capture_path)
+ it 'logs the rate limited analytics event for doc_auth' do
+ attach_and_submit_images
expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth document_capture visited',
- hash_including(redo_document_capture: true),
+ 'Rate Limit Reached',
+ limiter_type: :idv_doc_auth,
)
- DocAuth::Mock::DocAuthMockClient.reset!
- attach_and_submit_images
+ end
- expect(current_path).to eq(idv_ssn_path)
- expect(page).to have_css('[role="status"]') # We verified your ID
- complete_ssn_step
+ context 'successfully processes image on last attempt' do
+ before { DocAuth::Mock::DocAuthMockClient.reset! }
- expect(current_path).to eq(idv_verify_info_path)
- check t('forms.ssn.show')
- expect(page).to have_content(DocAuthHelper::GOOD_SSN)
+ it 'proceeds to the next page with valid info' do
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
+ attach_and_submit_images
+ expect(page).to have_current_path(idv_ssn_url)
+
+ visit idv_document_capture_path
+
+ expect(page).to have_current_path(idv_session_errors_rate_limited_path)
+ end
end
end
- end
- shared_examples_for 'image re-upload allowed' do
- it 'allows user to submit the same image again' do
- expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited')
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth image upload form submitted',
- hash_including(remaining_submit_attempts: 3),
+ it 'catches network connection errors on post_front_image', allow_browser_log: true do
+ DocAuth::Mock::DocAuthMockClient.mock_response!(
+ method: :post_front_image,
+ response: DocAuth::Response.new(
+ success: false,
+ errors: { network: I18n.t('doc_auth.errors.general.network_error') },
+ ),
)
- DocAuth::Mock::DocAuthMockClient.reset!
+
attach_and_submit_images
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth image upload form submitted',
- hash_including(remaining_submit_attempts: 2),
- )
- expect(current_path).to eq(idv_ssn_path)
- check t('forms.ssn.show')
- end
- end
- shared_examples_for 'image re-upload not allowed' do
- it 'stops user submitting the same image again' do
- expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited')
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth image upload form submitted',
- hash_including(remaining_submit_attempts: 3, submit_attempts: 1),
- )
- DocAuth::Mock::DocAuthMockClient.reset!
- attach_images
- # Error message without submit
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- )
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error'))
end
- end
- shared_examples_for 'document and selfie images re-upload not allowed' do
- it 'stops user submitting the same images again' do
- expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited')
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth image upload form submitted',
- hash_including(remaining_submit_attempts: 3, submit_attempts: 1),
- )
- DocAuth::Mock::DocAuthMockClient.reset!
- expect(page).not_to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- )
- attach_selfie
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- count: 1,
- )
+ it 'does not track state if state tracking is disabled' do
+ allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(false)
+ attach_and_submit_images
- attach_images
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- count: 3,
- )
+ expect(DocAuthLog.find_by(user_id: @user.id).state).to be_nil
end
end
- shared_examples_for 'inline error for 4xx status shown' do |status|
- it "shows inline error for status #{status}" do
- error = case status
- when 438
- t('doc_auth.errors.http.image_load.failed_short')
- when 439
- t('doc_auth.errors.http.pixel_depth.failed_short')
- when 440
- t('doc_auth.errors.http.image_size.failed_short')
- end
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: error,
- )
+ context 'standard mobile flow' do
+ it 'proceeds to the next page with valid info' do
+ perform_in_browser(:mobile) do
+ visit_idp_from_oidc_sp_with_ial2
+ sign_in_and_2fa_user(@user)
+ complete_doc_auth_steps_before_document_capture_step
+
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
+
+ # doc auth is successful while liveness is not req'd
+ use_id_image('ial2_test_credential_no_liveness.yml')
+ submit_images
+
+ expect(page).to have_current_path(idv_ssn_url)
+ expect_costing_for_document
+ expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY')
+
+ fill_out_ssn_form_ok
+ click_idv_continue
+ complete_verify_step
+ expect(page).to have_current_path(idv_phone_url)
+ end
end
end
- context 'error due to data issue with 2xx status code', allow_browser_log: true do
+
+ context 'facial match is required', allow_browser_log: true do
before do
- sign_in_and_2fa_user
+ allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
+ visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
+ sign_in_and_2fa_user(@user)
complete_doc_auth_steps_before_document_capture_step
- mock_general_doc_auth_client_error(:get_results)
- attach_and_submit_images
- click_try_again
end
- it_behaves_like 'image re-upload not allowed'
- end
- context 'error due to data issue with 4xx status code with trueid', allow_browser_log: true do
- before do
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_trueid_http_non2xx_status(438)
- attach_and_submit_images
- # verify it's a network error
- expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error'))
- click_try_again
+ it 'user can go through verification uploading ID and selfie on seprerate pages' do
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect(page).not_to have_content(t('doc_auth.tips.document_capture_selfie_text1'))
+ attach_images
+ click_continue
+ expect(page).to have_title(t('doc_auth.headings.selfie_capture'))
+ expect(page).to have_content(t('doc_auth.tips.document_capture_selfie_text1'))
+ click_button 'Take photo'
+ attach_selfie
+ submit_images
+ expect(page).to have_content(t('doc_auth.headings.capture_complete'))
end
- it_behaves_like 'image re-upload allowed'
+ it 'initial verification failure allows user to resubmit all images in 1 page' do
+ attach_images(
+ Rails.root.join(
+ 'spec', 'fixtures',
+ 'ial2_test_credential_multiple_doc_auth_failures_both_sides.yml'
+ ),
+ )
+ click_continue
+ click_button 'Take photo'
+ attach_selfie(
+ Rails.root.join(
+ 'spec', 'fixtures',
+ 'ial2_test_credential_forces_error.yml'
+ ),
+ )
+ submit_images
+ expect(page).to have_content(t('doc_auth.errors.rate_limited_heading'))
+ click_try_again
+ expect(page).to have_content(t('doc_auth.headings.review_issues'))
+ attach_images
+ submit_images
+ expect(page).to have_content(t('doc_auth.headings.capture_complete'))
+ end
end
- context 'error due to http status error but non 4xx status code with trueid',
- allow_browser_log: true do
+ context 'standard desktop flow' do
before do
- sign_in_and_2fa_user
+ visit_idp_from_oidc_sp_with_ial2
+ sign_in_and_2fa_user(@user)
complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_trueid_http_non2xx_status(500)
- attach_and_submit_images
- # verify it's a network error
- expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error'))
- click_try_again
end
- it_behaves_like 'image re-upload allowed'
- end
- context 'when selfie is enabled' do
- context 'when doc auth is success and face match fails (2xx)', allow_browser_log: true do
+ context 'rate limits calls to backend docauth vendor', allow_browser_log: true do
before do
- allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
- allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:fail)
- start_idv_from_sp(facial_match_required: true)
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_success_face_match_fail
- attach_images
- attach_selfie
- submit_images
- click_try_again
- sleep(10)
- end
-
- it_behaves_like 'document and selfie images re-upload not allowed'
+ allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts)
+ DocAuth::Mock::DocAuthMockClient.mock_response!(
+ method: :post_front_image,
+ response: DocAuth::Response.new(
+ success: false,
+ errors: { network: I18n.t('doc_auth.errors.general.network_error') },
+ ),
+ )
- it 'shows current existing header' do
- expect_doc_capture_page_header(t('doc_auth.headings.review_issues'))
+ (max_attempts - 1).times do
+ attach_and_submit_images
+ click_on t('idv.failure.button.warning')
+ end
end
- end
-
- context 'when doc auth passes and portrait match is not live', allow_browser_log: true do
- before do
- allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
- start_idv_from_sp(facial_match_required: true)
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_pass_and_portrait_match_not_live
- attach_images
- attach_selfie
- submit_images
- click_try_again
- sleep(10)
+ it 'redirects to the rate limited error page' do
+ freeze_time do
+ attach_and_submit_images
+ timeout = distance_of_time_in_words(
+ RateLimiter.attempt_window_in_minutes(:idv_doc_auth).minutes,
+ )
+ message = strip_tags(t('doc_auth.errors.rate_limited_text_html', timeout: timeout))
+ expect(page).to have_content(message)
+ expect(page).to have_current_path(idv_session_errors_rate_limited_path)
+ end
end
- it 'stops user submitting the same images again' do
- expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited')
+ it 'logs the rate limited analytics event for doc_auth' do
+ attach_and_submit_images
expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth image upload form submitted',
- hash_including(remaining_submit_attempts: 3, submit_attempts: 1),
- )
- DocAuth::Mock::DocAuthMockClient.reset!
- expect(page).not_to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
+ 'Rate Limit Reached',
+ limiter_type: :idv_doc_auth,
)
+ end
- attach_selfie
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- count: 1,
- )
+ context 'successfully processes image on last attempt' do
+ before { DocAuth::Mock::DocAuthMockClient.reset! }
- attach_images
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- count: 1,
- )
- end
- end
+ it 'proceeds to the next page with valid info' do
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
+ attach_and_submit_images
+ expect(page).to have_current_path(idv_ssn_url)
- context 'when doc auth fails and portrait match pass', allow_browser_log: true do
- before do
- allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
+ visit idv_document_capture_path
- start_idv_from_sp(facial_match_required: true)
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_failure_face_match_pass
- attach_images
- attach_selfie
- submit_images
- click_try_again
- sleep(10)
+ expect(page).to have_current_path(idv_session_errors_rate_limited_path)
+ end
end
+ end
- it 'stops user submitting the same images again' do
- expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited')
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth image upload form submitted',
- hash_including(remaining_submit_attempts: 3, submit_attempts: 1),
- )
- DocAuth::Mock::DocAuthMockClient.reset!
- expect(page).not_to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- )
+ it 'catches network connection errors on post_front_image', allow_browser_log: true do
+ DocAuth::Mock::DocAuthMockClient.mock_response!(
+ method: :post_front_image,
+ response: DocAuth::Response.new(
+ success: false,
+ errors: { network: I18n.t('doc_auth.errors.general.network_error') },
+ ),
+ )
- attach_selfie
- expect(page).not_to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- )
+ attach_and_submit_images
- attach_images
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- count: 2,
- )
- end
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error'))
end
- context 'when doc auth and portrait match fail', allow_browser_log: true do
- before do
- allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
- allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:fail)
- start_idv_from_sp(facial_match_required: true)
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_fail_face_match_fail
- attach_images
- attach_selfie
- submit_images
- click_try_again
- sleep(10)
- end
+ it 'does not track state if state tracking is disabled' do
+ allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(false)
+ attach_and_submit_images
- it_behaves_like 'document and selfie images re-upload not allowed'
+ expect(DocAuthLog.find_by(user_id: @user.id).state).to be_nil
end
+ end
- context 'when pii validation fails', allow_browser_log: true do
- before do
- allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
- pii = Idp::Constants::MOCK_IDV_APPLICANT.dup
- pii[:address1] = nil
- allow_any_instance_of(DocAuth::LexisNexis::Responses::TrueIdResponse).
- to receive(:pii_from_doc).and_return(Pii::StateId.new(**pii))
- start_idv_from_sp(facial_match_required: true)
- sign_in_and_2fa_user
+ context 'standard mobile flow' do
+ it 'proceeds to the next page with valid info' do
+ perform_in_browser(:mobile) do
+ visit_idp_from_oidc_sp_with_ial2
+ sign_in_and_2fa_user(@user)
complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_pass_face_match_pass_no_address1
- attach_images
- attach_selfie
- submit_images
- click_try_again
- sleep(10)
- end
- it 'shows selfie inline error messages for both front and back' do
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.general.multiple_front_id_failures'),
- count: 1,
- )
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.general.multiple_back_id_failures'),
- count: 1,
- )
- end
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
- it 'stops user submitting the same images again' do
- expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited')
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth image upload form submitted',
- hash_including(remaining_submit_attempts: 3, submit_attempts: 1),
- )
- DocAuth::Mock::DocAuthMockClient.reset!
- expect(page).not_to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- )
+ # doc auth is successful while liveness is not req'd
+ use_id_image('ial2_test_credential_no_liveness.yml')
+ submit_images
- attach_selfie
- expect(page).not_to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- )
+ expect(page).to have_current_path(idv_ssn_url)
+ expect_costing_for_document
+ expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY')
- attach_images
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- count: 2,
- )
+ fill_out_ssn_form_ok
+ click_idv_continue
+ complete_verify_step
+ expect(page).to have_current_path(idv_phone_url)
end
end
end
-
- context 'split doc auth flow', allow_browser_log: true do
+ context 'selfie check' do
before do
- allow(IdentityConfig.store).to receive(:doc_auth_separate_pages_enabled).and_return(true)
+ allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
end
- context 'when barcode scan returns a warning', allow_browser_log: true do
- let(:use_bad_ssn) { false }
- before do
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_attention_with_barcode
- attach_and_submit_images
- click_idv_continue
-
- if use_bad_ssn
- fill_out_ssn_form_with_ssn_that_fails_resolution
- else
- fill_out_ssn_form_ok
- end
-
- click_idv_continue
- end
-
- it 'shows a warning message to allow the user to return to upload new images' do
- warning_link_text = t('doc_auth.headings.capture_scan_warning_link')
-
- expect(page).to have_css(
- '[role="status"]',
- text: strip_nbsp(
- t(
- 'doc_auth.headings.capture_scan_warning_html',
- link_html: warning_link_text,
- ),
- ),
- )
- click_link warning_link_text
+ context 'when a selfie is not requested by SP' do
+ it 'proceeds to the next page with valid info, excluding a selfie image' do
+ perform_in_browser(:mobile) do
+ visit_idp_from_oidc_sp_with_ial2
+ sign_in_and_2fa_user(@user)
+ complete_doc_auth_steps_before_document_capture_step
- expect(current_path).to eq(idv_hybrid_handoff_path)
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth hybrid handoff visited',
- hash_including(redo_document_capture: true),
- )
- complete_hybrid_handoff_step
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth document_capture visited',
- hash_including(redo_document_capture: true),
- )
- DocAuth::Mock::DocAuthMockClient.reset!
- attach_and_submit_images
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
- expect(current_path).to eq(idv_ssn_path)
- expect(page).to have_css('[role="status"]') # We verified your ID
- complete_ssn_step
+ expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
- expect(current_path).to eq(idv_verify_info_path)
- check t('forms.ssn.show')
- expect(page).to have_content(DocAuthHelper::GOOD_SSN)
- end
+ attach_images
+ submit_images
- context 'with a bad SSN' do
- let(:use_bad_ssn) { true }
+ expect(page).to have_current_path(idv_ssn_url)
+ expect_costing_for_document
+ expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('MT')
- it 'shows a troubleshooting option to allow the user to cancel and return to SP' do
+ expect(page).to have_current_path(idv_ssn_url)
+ fill_out_ssn_form_ok
+ click_idv_continue
complete_verify_step
- expect(page).to have_link(
- t('links.cancel'),
- href: idv_cancel_path(step: :invalid_session),
- )
-
- click_link t('links.cancel')
-
- expect(current_path).to eq(idv_cancel_path)
+ # expect(page).to have_content(t('doc_auth.headings.document_capture_selfie'))
+ expect(page).to have_current_path(idv_phone_url)
end
end
+ end
- context 'on mobile', driver: :headless_chrome_mobile do
- it 'shows a warning message to allow the user to return to upload new images' do
- warning_link_text = t('doc_auth.headings.capture_scan_warning_link')
-
- expect(page).to have_css(
- '[role="status"]',
- text: strip_nbsp(
- t(
- 'doc_auth.headings.capture_scan_warning_html',
- link_html: warning_link_text,
- ),
- ),
- )
- click_link warning_link_text
+ context 'when a selfie is required by the SP' do
+ context 'on mobile platform', allow_browser_log: true do
+ before do
+ # mock mobile device as cameraCapable, this allows us to process
+ allow_any_instance_of(ActionController::Parameters).
+ to receive(:[]).and_wrap_original do |impl, param_name|
+ param_name.to_sym == :skip_hybrid_handoff ? '' : impl.call(param_name)
+ end
+ end
- expect(current_path).to eq(idv_document_capture_path)
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth document_capture visited',
- hash_including(redo_document_capture: true),
- )
- DocAuth::Mock::DocAuthMockClient.reset!
- attach_and_submit_images
+ context 'with a passing selfie' do
+ it 'proceeds to the next page with valid info, including a selfie image' do
+ perform_in_browser(:mobile) do
+ visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
+ sign_in_and_2fa_user(@user)
+ complete_doc_auth_steps_before_document_capture_step
+
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect(max_capture_attempts_before_native_camera.to_i).
+ to eq(ActiveSupport::Duration::SECONDS_PER_HOUR)
+ expect(max_submission_attempts_before_native_camera.to_i).
+ to eq(ActiveSupport::Duration::SECONDS_PER_HOUR)
+ expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
+ expect(page).to have_text(t('doc_auth.headings.document_capture'))
+ attach_images
+ click_continue
+ expect_doc_capture_selfie_subheader
+ click_button 'Take photo'
+ attach_selfie
+ submit_images
+
+ expect(page).to have_current_path(idv_ssn_url)
+ expect_costing_for_document
+ expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('MT')
+
+ expect(page).to have_current_path(idv_ssn_url)
+ fill_out_ssn_form_ok
+ click_idv_continue
+ complete_verify_step
+ expect(page).to have_current_path(idv_phone_url)
+ end
+ end
+ end
- expect(current_path).to eq(idv_ssn_path)
- expect(page).to have_css('[role="status"]') # We verified your ID
- complete_ssn_step
+ context 'documents or selfie with error is uploaded' do
+ shared_examples 'it has correct error displays' do
+ # when there are multiple doc auth errors on front and back
+ it 'shows the correct error message for the given error' do
+ perform_in_browser(:mobile) do
+ click_continue
+ use_id_image('ial2_test_credential_multiple_doc_auth_failures_both_sides.yml')
+ click_continue
+ click_button 'Take photo'
+ click_idv_submit_default
+ expect(page).not_to have_content(t('doc_auth.headings.capture_complete'))
+ expect(page).not_to have_content(t('doc_auth.errors.rate_limited_heading'))
+ expect(page).to have_title(t('doc_auth.headings.selfie_capture'))
+
+ use_selfie_image('ial2_test_credential_multiple_doc_auth_failures_both_sides.yml')
+ submit_images
+ expect_rate_limited_header(true)
+
+ expect_try_taking_new_pictures
+ expect_review_issues_body_message('doc_auth.errors.general.no_liveness')
+ expect_rate_limit_warning(max_attempts - 1)
+
+ expect_to_try_again
+ expect_resubmit_page_h1_copy
+
+ expect_resubmit_page_body_copy('doc_auth.errors.general.no_liveness')
+ expect_resubmit_page_inline_error_messages(2)
+ expect_resubmit_page_inline_selfie_error_message(false)
+
+ # Wrong doc type is uploaded
+ use_id_image('ial2_test_credential_wrong_doc_type.yml')
+
+ use_selfie_image('ial2_test_portrait_match_success.yml')
+ submit_images
+
+ expect_rate_limited_header(false)
+ expect_try_taking_new_pictures(false)
+ # eslint-disable-next-line
+ expect_review_issues_body_message(
+ 'doc_auth.errors.doc_type_not_supported_heading',
+ )
+ expect_review_issues_body_message('doc_auth.errors.doc.doc_type_check')
+ expect_rate_limit_warning(max_attempts - 2)
+
+ expect_to_try_again
+ expect_resubmit_page_h1_copy
+
+ expect_review_issues_body_message('doc_auth.errors.card_type')
+ expect_resubmit_page_inline_selfie_error_message(false)
+
+ # when there are multiple front doc auth errors
+ use_id_image(
+ 'ial2_test_credential_multiple_doc_auth_failures_front_side_only.yml',
+ )
+
+ use_selfie_image(
+ 'ial2_test_credential_multiple_doc_auth_failures_front_side_only.yml',
+ )
+ submit_images
+
+ expect_rate_limited_header(true)
+ expect_try_taking_new_pictures(false)
+ expect_review_issues_body_message(
+ 'doc_auth.errors.general.multiple_front_id_failures',
+ )
+ expect_rate_limit_warning(max_attempts - 3)
+
+ expect_to_try_again
+ expect_resubmit_page_h1_copy
+
+ expect_resubmit_page_body_copy(
+ 'doc_auth.errors.general.multiple_front_id_failures',
+ )
+ expect_resubmit_page_inline_error_messages(1)
+ expect_resubmit_page_inline_selfie_error_message(false)
+
+ # when there are multiple back doc auth errors
+ use_id_image(
+ 'ial2_test_credential_multiple_doc_auth_failures_back_side_only.yml',
+ )
+
+ use_selfie_image(
+ 'ial2_test_credential_multiple_doc_auth_failures_back_side_only.yml',
+ )
+ submit_images
+
+ expect_rate_limited_header(true)
+ expect_try_taking_new_pictures(false)
+ expect_review_issues_body_message(
+ 'doc_auth.errors.general.multiple_back_id_failures',
+ )
+ expect_rate_limit_warning(max_attempts - 4)
+
+ expect_to_try_again
+ expect_resubmit_page_h1_copy
+
+ expect_resubmit_page_body_copy(
+ 'doc_auth.errors.general.multiple_back_id_failures',
+ )
+ expect_resubmit_page_inline_error_messages(1)
+ expect_resubmit_page_inline_selfie_error_message(false)
+
+ # attention barcode with invalid pii is uploaded
+ use_id_image('ial2_test_credential_barcode_attention_no_address.yml')
+ click_continue
+ use_selfie_image('ial2_test_portrait_match_success.yml')
+ submit_images
+
+ expect(page).to have_content(t('doc_auth.errors.alerts.address_check'))
+ expect(page).to have_current_path(idv_document_capture_path)
+
+ click_try_again
+
+ # And finally, after lots of errors, we can still succeed
+ attach_images
+ submit_images
+
+ expect(page).to have_current_path(idv_ssn_path)
+ end
+ end
+ end
+
+ context 'IPP enabled' do
+ let(:ipp_service_provider) do
+ create(:service_provider, :active, :in_person_proofing_enabled)
+ end
+
+ 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_any_instance_of(ServiceProvider).to receive(
+ :in_person_proofing_enabled,
+ ).and_return(true)
+ allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(99)
+ 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])
+ perform_in_browser(:mobile) do
+ visit_idp_from_sp_with_ial2(
+ :oidc,
+ **{ client_id: ipp_service_provider.issuer,
+ facial_match_required: true },
+ )
+ sign_in_and_2fa_user(@user)
+ complete_up_to_how_to_verify_step_for_opt_in_ipp(
+ facial_match_required: true,
+ )
+ end
+ end
+
+ it_should_behave_like 'it has correct error displays'
+ end
+
+ context 'IPP not enabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(99)
+ perform_in_browser(:mobile) do
+ visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
+ sign_in_and_2fa_user(@user)
+ complete_doc_auth_steps_before_document_capture_step
+ end
+ end
- expect(current_path).to eq(idv_verify_info_path)
- check t('forms.ssn.show')
- expect(page).to have_content(DocAuthHelper::GOOD_SSN)
+ it_should_behave_like 'it has correct error displays'
+ end
end
- end
- end
- shared_examples_for 'image re-upload allowed' do
- it 'allows user to submit the same image again' do
- expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited')
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth image upload form submitted',
- hash_including(remaining_submit_attempts: 3),
- )
- DocAuth::Mock::DocAuthMockClient.reset!
- attach_and_submit_images
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth image upload form submitted',
- hash_including(remaining_submit_attempts: 2),
- )
- expect(current_path).to eq(idv_ssn_path)
- check t('forms.ssn.show')
+ context 'when selfie check is not enabled (flag off, and/or in production)' do
+ it 'proceeds to the next page with valid info, excluding a selfie image' do
+ perform_in_browser(:mobile) do
+ visit_idp_from_oidc_sp_with_ial2
+ sign_in_and_2fa_user(@user)
+ complete_doc_auth_steps_before_document_capture_step
+
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect(max_capture_attempts_before_native_camera).to eq(
+ IdentityConfig.store.doc_auth_max_capture_attempts_before_native_camera.to_s,
+ )
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
+
+ expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
+
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
+ attach_images
+ submit_images
+
+ expect(page).to have_current_path(idv_ssn_url)
+ expect_costing_for_document
+ expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('MT')
+
+ expect(page).to have_current_path(idv_ssn_url)
+ fill_out_ssn_form_ok
+ click_idv_continue
+ complete_verify_step
+ expect(page).to have_current_path(idv_phone_url)
+ end
+ end
+ end
end
- end
- shared_examples_for 'image re-upload not allowed' do
- it 'stops user submitting the same image again' do
- expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited')
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth image upload form submitted',
- hash_including(remaining_submit_attempts: 3, submit_attempts: 1),
- )
- DocAuth::Mock::DocAuthMockClient.reset!
- attach_images
- # Error message without submit
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- )
- end
- end
+ context 'on desktop' do
+ let(:desktop_selfie_mode) { false }
- shared_examples_for 'document and selfie images re-upload not allowed' do
- it 'stops user submitting the same images again' do
- expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited')
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth image upload form submitted',
- hash_including(remaining_submit_attempts: 3, submit_attempts: 1),
- )
- DocAuth::Mock::DocAuthMockClient.reset!
- expect(page).not_to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- )
- attach_selfie
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- count: 1,
- )
+ before do
+ allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode).
+ and_return(desktop_selfie_mode)
+ end
- attach_images
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- count: 3,
- )
- end
- end
+ describe 'when desktop selfie not allowed' do
+ it 'can only proceed to link sent page' do
+ perform_in_browser(:desktop) do
+ visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
+ sign_in_and_2fa_user(@user)
+ complete_doc_auth_steps_before_hybrid_handoff_step
+ # we still have option to continue
+ expect(page).to have_current_path(idv_hybrid_handoff_path)
+ expect(page).to have_content(t('doc_auth.headings.hybrid_handoff_selfie'))
+ expect(page).not_to have_content(t('doc_auth.headings.hybrid_handoff'))
+ expect(page).not_to have_content(t('doc_auth.info.upload_from_computer'))
+ click_on t('forms.buttons.send_link')
+ expect(page).to have_current_path(idv_link_sent_path)
+ end
+ end
+ end
- shared_examples_for 'inline error for 4xx status shown' do |status|
- it "shows inline error for status #{status}" do
- error = case status
- when 438
- t('doc_auth.errors.http.image_load.failed_short')
- when 439
- t('doc_auth.errors.http.pixel_depth.failed_short')
- when 440
- t('doc_auth.errors.http.image_size.failed_short')
+ describe 'when desktop selfie is allowed' do
+ let(:desktop_selfie_mode) { true }
+
+ it 'proceed to the next page with valid info, including a selfie image' do
+ perform_in_browser(:desktop) do
+ visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
+ sign_in_and_2fa_user(@user)
+ complete_doc_auth_steps_before_hybrid_handoff_step
+ # we still have option to continue on handoff, since it's desktop no skip_hand_off
+ expect(page).to have_current_path(idv_hybrid_handoff_path)
+ expect(page).to have_content(t('doc_auth.headings.hybrid_handoff_selfie'))
+ expect(page).not_to have_content(t('doc_auth.headings.hybrid_handoff'))
+ expect(page).to have_content(t('doc_auth.info.upload_from_computer'))
+ click_on t('forms.buttons.upload_photos')
+ expect(page).to have_current_path(idv_document_capture_url)
+ expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
+ expect(page).to have_text(t('doc_auth.headings.document_capture'))
+ attach_images
+ click_continue
+ expect_doc_capture_selfie_subheader
+ click_button 'Take photo'
+ attach_selfie
+ submit_images
+
+ expect(page).to have_current_path(idv_ssn_url)
+ expect_costing_for_document
+ expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('MT')
+
+ expect(page).to have_current_path(idv_ssn_url)
+ fill_out_ssn_form_ok
+ click_idv_continue
+ complete_verify_step
+ expect(page).to have_current_path(idv_phone_url)
+ end
+ end
+
+ context 'when ipp is enabled' do
+ let(:in_person_doc_auth_button_enabled) { true }
+ let(:sp_ipp_enabled) { true }
+
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_doc_auth_button_enabled).
+ and_return(in_person_doc_auth_button_enabled)
+ allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).with(anything).
+ and_return(sp_ipp_enabled)
+ end
+
+ describe 'when ipp is selected' do
+ it 'proceed to the next page and start ipp' do
+ perform_in_browser(:desktop) do
+ visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
+ sign_in_and_2fa_user(@user)
+ complete_doc_auth_steps_before_hybrid_handoff_step
+ # still have option to continue handoff, since it's desktop no skip_hand_off
+ expect(page).to have_current_path(idv_hybrid_handoff_path)
+ expect(page).to have_content(t('doc_auth.headings.hybrid_handoff_selfie'))
+ click_on t('in_person_proofing.headings.prepare')
+ expect(page).to have_current_path(
+ idv_document_capture_path({ step: 'hybrid_handoff' }),
+ )
+ expect_step_indicator_current_step(
+ t('step_indicator.flows.idv.find_a_post_office'),
+ )
+ expect_doc_capture_page_header(t('in_person_proofing.headings.prepare'))
end
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: error,
- )
- end
- end
- context 'error due to data issue with 2xx status code', allow_browser_log: true do
- before do
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_general_doc_auth_client_error(:get_results)
- attach_and_submit_images
- click_try_again
+ end
+ end
+ end
+ end
end
- it_behaves_like 'image re-upload not allowed'
end
+ end
- context 'error due to data issue with 4xx status code with trueid', allow_browser_log: true do
- before do
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_trueid_http_non2xx_status(438)
- attach_and_submit_images
- # verify it's a network error
- expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error'))
- click_try_again
- end
-
- it_behaves_like 'image re-upload allowed'
+ def expect_rate_limited_header(expected_to_be_present)
+ review_issues_h1_heading = strip_tags(t('doc_auth.errors.rate_limited_heading'))
+ if expected_to_be_present
+ expect(page).to have_content(review_issues_h1_heading)
+ else
+ expect(page).not_to have_content(review_issues_h1_heading)
end
+ end
- context 'error due to http status error but non 4xx status code with trueid',
- allow_browser_log: true do
- before do
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_trueid_http_non2xx_status(500)
- attach_and_submit_images
- # verify it's a network error
- expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error'))
- click_try_again
- end
- it_behaves_like 'image re-upload allowed'
+ def expect_try_taking_new_pictures(expected_to_be_present = true)
+ expected_message = strip_tags(
+ t('doc_auth.errors.rate_limited_subheading'),
+ )
+ if expected_to_be_present
+ expect(page).to have_content expected_message
+ else
+ expect(page).not_to have_content expected_message
end
+ end
- context 'when selfie is enabled' do
- context 'when doc auth is success and face match fails (2xx)', allow_browser_log: true do
- before do
- allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
- allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:fail)
- start_idv_from_sp(facial_match_required: true)
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_success_face_match_fail
- attach_images
- click_continue
- attach_selfie
- submit_images
- click_try_again
- sleep(10)
- end
-
- it_behaves_like 'document and selfie images re-upload not allowed'
-
- it 'shows current existing header' do
- expect_doc_capture_page_header(t('doc_auth.headings.review_issues'))
- end
- end
+ def expect_review_issues_body_message(translation_key)
+ review_issues_body_message = strip_tags(t(translation_key))
+ expect(page).to have_content(review_issues_body_message)
+ end
- context 'when doc auth passes and portrait match is not live', allow_browser_log: true do
- before do
- allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
+ def expect_rate_limit_warning(expected_remaining_attempts)
+ review_issues_rate_limit_warning = strip_tags(
+ t(
+ 'idv.failure.attempts_html',
+ count: expected_remaining_attempts,
+ ),
+ )
+ expect(page).to have_content(review_issues_rate_limit_warning)
+ end
- start_idv_from_sp(facial_match_required: true)
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_pass_and_portrait_match_not_live
- attach_images
- click_continue
- attach_selfie
- submit_images
- click_try_again
- sleep(10)
- end
+ def expect_resubmit_page_h1_copy
+ resubmit_page_h1_copy = strip_tags(t('doc_auth.headings.review_issues'))
+ expect(page).to have_content(resubmit_page_h1_copy)
+ end
- it 'stops user submitting the same images again' do
- expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited')
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth image upload form submitted',
- hash_including(remaining_submit_attempts: 3, submit_attempts: 1),
- )
- DocAuth::Mock::DocAuthMockClient.reset!
- expect(page).not_to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- )
+ def expect_resubmit_page_body_copy(translation_key)
+ resubmit_page_body_copy = strip_tags(t(translation_key))
+ expect(page).to have_content(resubmit_page_body_copy)
+ end
- attach_selfie
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- count: 1,
- )
+ def expect_resubmit_page_inline_error_messages(expected_count)
+ resubmit_page_inline_error_messages = strip_tags(
+ t('doc_auth.errors.general.fallback_field_level'),
+ )
+ expect(page).to have_content(resubmit_page_inline_error_messages).exactly(expected_count)
+ end
- attach_images
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- count: 1,
- )
- end
- end
+ def expect_resubmit_page_inline_selfie_error_message(should_be_present)
+ resubmit_page_inline_selfie_error_message = strip_tags(
+ t('doc_auth.errors.general.selfie_failure'),
+ )
+ if should_be_present
+ expect(page).to have_content(resubmit_page_inline_selfie_error_message)
+ else
+ expect(page).not_to have_content(resubmit_page_inline_selfie_error_message)
+ end
+ end
- context 'when doc auth fails and portrait match pass', allow_browser_log: true do
- before do
- allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
+ def expect_to_try_again
+ click_try_again
+ expect(page).to have_current_path(idv_document_capture_path)
+ end
- start_idv_from_sp(facial_match_required: true)
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_failure_face_match_pass
- attach_images
- click_continue
- attach_selfie
- submit_images
- click_try_again
- sleep(10)
- end
+ def use_id_image(filename)
+ expect(page).to have_content('Front of your ID')
+ attach_images Rails.root.join('spec', 'fixtures', filename)
+ end
- it 'stops user submitting the same images again' do
- expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited')
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth image upload form submitted',
- hash_including(remaining_submit_attempts: 3, submit_attempts: 1),
- )
- DocAuth::Mock::DocAuthMockClient.reset!
- expect(page).not_to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- )
+ def use_selfie_image(filename)
+ attach_selfie Rails.root.join('spec', 'fixtures', filename)
+ end
- attach_selfie
- expect(page).not_to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- )
+ def expect_costing_for_document
+ %i[acuant_front_image acuant_back_image acuant_result].each do |cost_type|
+ expect(costing_for(cost_type)).to be_present
+ end
+ end
- attach_images
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- count: 2,
- )
- end
- end
+ def costing_for(cost_type)
+ SpCost.where(ial: 2, issuer: 'urn:gov:gsa:openidconnect:sp:server', cost_type: cost_type.to_s)
+ end
+end
- context 'when doc auth and portrait match fail', allow_browser_log: true do
- before do
- allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
- allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:fail)
- start_idv_from_sp(facial_match_required: true)
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_fail_face_match_fail
- attach_images
- click_continue
- attach_selfie
- submit_images
- click_try_again
- sleep(10)
- end
+RSpec.feature 'direct access to IPP on desktop', :js do
+ include IdvStepHelper
+ include DocAuthHelper
- it_behaves_like 'document and selfie images re-upload not allowed'
- end
+ context 'before handoff page' do
+ let(:sp_ipp_enabled) { true }
+ let(:in_person_proofing_opt_in_enabled) { true }
+ let(:facial_match_required) { true }
+ let(:user) { user_with_2fa }
- context 'when pii validation fails', allow_browser_log: true do
- before do
- allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
- pii = Idp::Constants::MOCK_IDV_APPLICANT.dup
- pii[:address1] = nil
- allow_any_instance_of(DocAuth::LexisNexis::Responses::TrueIdResponse).
- to receive(:pii_from_doc).and_return(Pii::StateId.new(**pii))
- start_idv_from_sp(facial_match_required: true)
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_pass_face_match_pass_no_address1
- attach_images
- click_continue
- attach_selfie
- submit_images
- click_try_again
- sleep(10)
- end
+ before do
+ service_provider = create(:service_provider, :active, :in_person_proofing_enabled)
+ allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode).and_return(false)
+ 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(
+ in_person_proofing_opt_in_enabled,
+ )
+ allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers).
+ and_return([service_provider.issuer])
+ allow(IdentityConfig.store).to receive(
+ :allowed_valid_authn_contexts_semantic_providers,
+ ).and_return([service_provider.issuer])
+ allow_any_instance_of(ServiceProvider).to receive(:in_person_proofing_enabled).
+ and_return(false)
+ visit_idp_from_sp_with_ial2(
+ :oidc,
+ **{ client_id: service_provider.issuer,
+ facial_match_required: facial_match_required },
+ )
+ sign_in_via_branded_page(user)
+ complete_doc_auth_steps_before_agreement_step
- it 'shows selfie inline error messages for both front and back' do
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.general.multiple_front_id_failures'),
- count: 1,
- )
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.general.multiple_back_id_failures'),
- count: 1,
- )
- end
+ visit idv_document_capture_path(step: 'hybrid_handoff')
+ end
- it 'stops user submitting the same images again' do
- expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited')
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth image upload form submitted',
- hash_including(remaining_submit_attempts: 3, submit_attempts: 1),
- )
- DocAuth::Mock::DocAuthMockClient.reset!
- expect(page).not_to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- )
+ context 'when selfie is enabled' do
+ it 'redirects back to agreement page' do
+ expect(page).to have_current_path(idv_agreement_path)
+ end
+ end
- attach_selfie
- expect(page).not_to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- )
+ context 'when selfie is disabled' do
+ let(:facial_match_required) { false }
- attach_images
- expect(page).to have_css(
- '.usa-error-message[role="alert"]',
- text: t('doc_auth.errors.doc.resubmit_failed_image'),
- count: 2,
- )
- end
+ it 'redirects back to agreement page' do
+ expect(page).to have_current_path(idv_agreement_path)
end
end
end
diff --git a/spec/features/idv/get_proofing_results_job_scenarios_spec.rb b/spec/features/idv/get_proofing_results_job_scenarios_spec.rb
new file mode 100644
index 00000000000..e98b057d677
--- /dev/null
+++ b/spec/features/idv/get_proofing_results_job_scenarios_spec.rb
@@ -0,0 +1,739 @@
+require 'rails_helper'
+require 'axe-rspec'
+
+RSpec.feature 'GetUspsProofingResultsJob Scenarios', js: true, allowed_extra_analytics: [:*] do
+ include OidcAuthHelper
+ include UspsIppHelper
+ include ActiveJob::TestHelper
+
+ background do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
+ allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false)
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enforce_tmx).and_return(true)
+ stub_request_token
+ ActiveJob::Base.queue_adapter = :test
+ end
+
+ feature 'before/after password reset:' do
+ background do
+ @user = create(:user, :with_phone, :with_pending_in_person_enrollment)
+ @new_password = '$alty pickles'
+ end
+
+ scenario 'User resets password and logs in before USPS proofing "passed"' do
+ # Given the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated pending in person verification
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ )
+
+ # When the user resets their password
+ reset_password(@user, @new_password)
+
+ # Then the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated pending in person verification
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ )
+
+ # When the user logs in
+ login(@user, @new_password)
+
+ # Then the user is taken to the /verify/welcome page
+ expect(current_path).to eq(idv_welcome_path)
+ # And the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated with reason "encryption_error"
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ in_person_verification_pending_at: be_kind_of(Time),
+ )
+
+ # When the user logs out
+ logout(@user)
+ # And the user visits USPS to complete their enrollment
+ # And USPS enrollment passed
+ stub_request_passed_proofing_results
+ # And GetUspsProofingResultsJob is performed
+ perform_get_usps_proofing_results_job(@user)
+
+ # And the user has an InPersonEnrollment with status "passed"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'passed',
+ )
+ # And the user has a Profile that is active
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: true,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: nil,
+ )
+
+ # When the user logs in
+ login(@user, @new_password)
+
+ # Then the user is taken to the /verify/welcome page
+ expect(current_path).to eq(idv_welcome_path)
+ # And the user has an InPersonEnrollment with status "passed"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'passed',
+ )
+ # And the user has a Profile that is deactivated with reason "encryption_error"
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ in_person_verification_pending_at: nil,
+ )
+ end
+
+ ['failed', 'cancelled', 'expired'].each do |status|
+ scenario "User resets password and logs in before USPS proofing \"#{status}\"" do
+ # Given the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated pending in person verification
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ )
+
+ # When the user resets their password
+ reset_password(@user, @new_password)
+
+ # Then the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a deactivated profile due to in person verification
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ )
+
+ # When the user logs in
+ login(@user, @new_password)
+
+ # Then the user is taken to the /verify/welcome page
+ expect(current_path).to eq(idv_welcome_path)
+ # And the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated with reason "encryption_error"
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ in_person_verification_pending_at: be_kind_of(Time),
+ )
+
+ # When the user logs out
+ logout(@user)
+ # And the user visits USPS to complete their enrollment
+ # And USPS enrollment "failed|cancelled|expired"
+ stub_request_proofing_results(status, @user.in_person_enrollments.first.enrollment_code)
+ # And GetUspsProofingResultsJob is performed
+ perform_get_usps_proofing_results_job(@user)
+
+ # Then the user has an InPersonEnrollment with status "failed|cancelled|expired"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: status,
+ )
+ # And the user has a Profile that is deactivated with reason "encryption_error"
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ in_person_verification_pending_at: nil,
+ )
+
+ # When the user logs in
+ login(@user, @new_password)
+
+ # Then the user is taken to the /verify/welcome page
+ expect(current_path).to eq(idv_welcome_path)
+ # And the user has an InPersonEnrollment with status "failed|cancelled|expired"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: status,
+ )
+ # And the user has a Profile that is deactivated with reason "encryption_error"
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ in_person_verification_pending_at: nil,
+ )
+ end
+ end
+
+ scenario 'User resets password without logging in before USPS proofing "passed"' do
+ # Given the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated pending in person verification
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ )
+
+ # When the user resets their password
+ reset_password(@user, @new_password)
+
+ # Then the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a deactivated profile due to in person verification
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ )
+
+ # And the user visits USPS to complete their enrollment
+ # And USPS enrollment passed
+ stub_request_passed_proofing_results
+ # And GetUspsProofingResultsJob is performed
+ perform_get_usps_proofing_results_job(@user)
+
+ # Then the user has an InPersonEnrollment with status "passed"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'passed',
+ )
+ # And the user has a Profile that is active
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: true,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: nil,
+ )
+
+ # When the user logs in
+ login(@user, @new_password)
+
+ # Then the user is taken to the /verify/welcome page
+ expect(current_path).to eq(idv_welcome_path)
+ # And the user has an InPersonEnrollment with status "passed"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'passed',
+ )
+ # And the user has a Profile that is deactivated with reason "encryption_error"
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ in_person_verification_pending_at: nil,
+ )
+ end
+
+ ['failed', 'cancelled', 'expired'].each do |status|
+ scenario "User resets password without logging in before USPS proofing \"#{status}\"" do
+ # Given the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated pending in person verification
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ )
+
+ # When the user resets their password
+ reset_password(@user, @new_password)
+
+ # Then the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated pending in person verification
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ )
+
+ # And the user visits USPS to complete their enrollment
+ # And USPS enrollment "failed|cancelled|expired"
+ stub_request_proofing_results(status, @user.in_person_enrollments.first.enrollment_code)
+ # And GetUspsProofingResultsJob is performed
+ perform_get_usps_proofing_results_job(@user)
+
+ # Then the user has an InPersonEnrollment with status "failed|cancelled|expired"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: status,
+ )
+ # And the user has a Profile that is deactivated with reason "verification_cancelled"
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'verification_cancelled',
+ in_person_verification_pending_at: nil,
+ )
+
+ # When the user logs in
+ login(@user, @new_password)
+
+ # Then the user is taken to the /verify/welcome page
+ expect(current_path).to eq(idv_welcome_path)
+ # And the user has an InPersonEnrollment with status "failed|cancelled|expired"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: status,
+ )
+ # And the user has a Profile that is deactivated with reason "verification_cancelled"
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'verification_cancelled',
+ in_person_verification_pending_at: nil,
+ )
+ end
+ end
+
+ scenario 'User resets password with personal key after USPS proofing "passed"' do
+ # Given the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated pending in person verification
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ )
+
+ # When the user visits USPS to complete their enrollment
+ # And USPS enrollment passed
+ stub_request_passed_proofing_results
+ # And GetUspsProofingResultsJob is performed
+ perform_get_usps_proofing_results_job(@user)
+
+ # Then the user has an InPersonEnrollment with status "passed"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'passed',
+ )
+ # And the user has a Profile that is active
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: true,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: nil,
+ )
+
+ # When the user resets their password
+ reset_password(@user, @new_password)
+
+ # Then the user has an InPersonEnrollment with status "passed"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'passed',
+ )
+ # And the user has a Profile that is deactivated with reason "password_reset"
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'password_reset',
+ in_person_verification_pending_at: nil,
+ )
+
+ # When the user logs in
+ login(@user, @new_password)
+ # Then the user is taken to the /account/reactivate/start page
+ expect(current_path).to eq(reactivate_account_path)
+
+ # When the user attempts to reactivate account without their personal key
+ account_reactivation_with_personal_key(@user, @new_password)
+
+ # Then the user is taken to the /sign_up/completed page
+ expect(current_path).to eq(sign_up_completed_path)
+ # And the user has an InPersonEnrollment with status "passed"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'passed',
+ )
+ # And the user has a Profile that is active
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: true,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: nil,
+ )
+ end
+
+ scenario 'User resets password without personal key after USPS proofing "passed"' do
+ # Given the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated pending in person verification
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ )
+
+ # When the user visits USPS to complete their enrollment
+ # And USPS enrollment passed
+ stub_request_passed_proofing_results
+ # And GetUspsProofingResultsJob is performed
+ perform_get_usps_proofing_results_job(@user)
+
+ # Then the user has an InPersonEnrollment with status "passed"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'passed',
+ )
+ # And the user has a Profile that is active
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: true,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: nil,
+ )
+
+ # When the user resets their password
+ reset_password(@user, @new_password)
+
+ # And the user has an InPersonEnrollment with status "passed"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'passed',
+ )
+ # And the user has a Profile that is deactivated with reason "password_reset"
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'password_reset',
+ in_person_verification_pending_at: nil,
+ )
+
+ # When the user logs in
+ login(@user, @new_password)
+ # Then the user is taken to the /account/reactivate/start page
+ expect(current_path).to eq(reactivate_account_path)
+
+ # When the user attempts to reactivate account without their personal key
+ account_reactivation_without_personal_key
+
+ # Then the user is taken to the /verify/welcome page
+ expect(current_path).to eq(idv_welcome_path)
+ # And the user has an InPersonEnrollment with status "passed"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'passed',
+ )
+ # And the user has a Profile that is deactivated with reason "password_reset"
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'password_reset',
+ in_person_verification_pending_at: nil,
+ )
+ end
+
+ ['failed', 'cancelled', 'expired'].each do |status|
+ scenario "User resets password after USPS proofing \"#{status}\"" do
+ # Given the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated pending in person verification
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ )
+
+ # When the user visits USPS to complete their enrollment
+ # And USPS enrollment "failed|cancelled|expired"
+ stub_request_proofing_results(status, @user.in_person_enrollments.first.enrollment_code)
+ # And GetUspsProofingResultsJob is performed
+ perform_get_usps_proofing_results_job(@user)
+
+ # Then the user has an InPersonEnrollment with status "failed|cancelled|expired"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: status,
+ )
+ # And the user has a Profile that is deactivated with reason "verification_cancelled"
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'verification_cancelled',
+ in_person_verification_pending_at: nil,
+ )
+
+ # When the user resets their password
+ reset_password(@user, @new_password)
+
+ # Then the user has an InPersonEnrollment with status "failed|cancelled|expired"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: status,
+ )
+ # And the user has a Profile that is deactivated with reason "verification_cancelled"
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'verification_cancelled',
+ in_person_verification_pending_at: nil,
+ )
+
+ # When the user logs in
+ login(@user, @new_password)
+
+ # Then the user is taken to the /verify/welcome page
+ expect(current_path).to eq(idv_welcome_path)
+ # And the user has an InPersonEnrollment with status "failed|cancelled|expired"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: status,
+ )
+ # And the user has a Profile that is deactivated with reason "verification_cancelled"
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'verification_cancelled',
+ in_person_verification_pending_at: nil,
+ )
+ end
+ end
+
+ scenario <<~EOS.squish do
+ User resets password and logs in before USPS proofing "passed" with fraud review pending
+ EOS
+ @user.in_person_enrollments.first.profile.update(
+ fraud_review_pending_at: 1.day.ago,
+ fraud_pending_reason: 'threatmetrix_review',
+ proofing_components: { threatmetrix_review_status: 'review' },
+ )
+
+ # Given the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated pending in person verification and
+ # fraud review
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ fraud_pending_reason: 'threatmetrix_review',
+ fraud_review_pending_at: be_kind_of(Time),
+ )
+
+ # When the user resets their password
+ reset_password(@user, @new_password)
+
+ # Then the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated pending in person verification and
+ # fraud review
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ fraud_pending_reason: 'threatmetrix_review',
+ fraud_review_pending_at: be_kind_of(Time),
+ )
+
+ # When the user logs in
+ login(@user, @new_password)
+
+ # Then the user is taken to the /verify/welcome page
+ expect(current_path).to eq(idv_welcome_path)
+ # And the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated with reason "encryption_error" and
+ # pending in person verification and fraud review
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ in_person_verification_pending_at: be_kind_of(Time),
+ fraud_pending_reason: 'threatmetrix_review',
+ fraud_review_pending_at: be_kind_of(Time),
+ )
+
+ # When the user logs out
+ logout(@user)
+ # And the user visits USPS to complete their enrollment
+ # And USPS enrollment passed
+ stub_request_passed_proofing_results
+ # And GetUspsProofingResultsJob is performed
+ perform_get_usps_proofing_results_job(@user)
+
+ # And the user has an InPersonEnrollment with status "passed"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'passed',
+ )
+ # And the user has a Profile that is deactivated with reason "encryption_error" and
+ # pending fraud review
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ in_person_verification_pending_at: nil,
+ fraud_pending_reason: 'threatmetrix_review',
+ fraud_review_pending_at: be_kind_of(Time),
+ )
+
+ # When the user logs in
+ login(@user, @new_password)
+
+ # Then the user is taken to the /verify/welcome page
+ expect(current_path).to eq(idv_welcome_path)
+ # And the user has an InPersonEnrollment with status "passed"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'passed',
+ )
+ # And the user has a Profile that is deactivated with reason "encryption_error" and
+ # pending fraud review
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ in_person_verification_pending_at: nil,
+ fraud_pending_reason: 'threatmetrix_review',
+ fraud_review_pending_at: be_kind_of(Time),
+ )
+ end
+
+ scenario <<~EOS.squish do
+ User resets password without logging in before USPS proofing "passed" with fraud review pending
+ EOS
+ @user.in_person_enrollments.first.profile.update(
+ fraud_review_pending_at: 1.day.ago,
+ fraud_pending_reason: 'threatmetrix_review',
+ proofing_components: { threatmetrix_review_status: 'review' },
+ )
+
+ # Given the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated pending in person verification and
+ # fraud review
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ fraud_pending_reason: 'threatmetrix_review',
+ fraud_review_pending_at: be_kind_of(Time),
+ )
+
+ # When the user resets their password
+ reset_password(@user, @new_password)
+
+ # Then the user has an InPersonEnrollment with status "pending"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'pending',
+ )
+ # And the user has a Profile that is deactivated pending in person verification and
+ # fraud review
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: be_kind_of(Time),
+ fraud_pending_reason: 'threatmetrix_review',
+ fraud_review_pending_at: be_kind_of(Time),
+ )
+
+ # And the user visits USPS to complete their enrollment
+ # And USPS enrollment passed
+ stub_request_passed_proofing_results
+ # And GetUspsProofingResultsJob is performed
+ perform_get_usps_proofing_results_job(@user)
+
+ # Then the user has an InPersonEnrollment with status "passed"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'passed',
+ )
+ # And the user has a Profile that is deactivated pending fraud review
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: nil,
+ in_person_verification_pending_at: nil,
+ fraud_pending_reason: 'threatmetrix_review',
+ fraud_review_pending_at: be_kind_of(Time),
+ )
+
+ # When the user logs in
+ login(@user, @new_password)
+
+ # Then the user is taken to the /verify/welcome page
+ expect(current_path).to eq(idv_welcome_path)
+ # And the user has an InPersonEnrollment with status "passed"
+ expect(@user.in_person_enrollments.first).to have_attributes(
+ status: 'passed',
+ )
+ # And the user has a Profile that is deactivated with reason "encryption_error" and
+ # pending fraud review
+ expect(@user.in_person_enrollments.first.profile).to have_attributes(
+ active: false,
+ deactivation_reason: 'encryption_error',
+ in_person_verification_pending_at: nil,
+ fraud_pending_reason: 'threatmetrix_review',
+ fraud_review_pending_at: be_kind_of(Time),
+ )
+ end
+ end
+
+ def reset_password(user, new_password)
+ visit_idp_from_ial2_oidc_sp
+ fill_forgot_password_form(user)
+ click_reset_password_link_from_email
+
+ fill_in t('forms.passwords.edit.labels.password'), with: new_password
+ fill_in t('components.password_confirmation.confirm_label'),
+ with: new_password
+ fill_in t('components.password_confirmation.confirm_label'),
+ with: new_password
+ click_on t('forms.passwords.edit.buttons.submit')
+ user.reload
+ end
+
+ def account_reactivation_without_personal_key
+ click_on t('links.account.reactivate.without_key')
+ click_on t('forms.buttons.continue')
+ end
+
+ def account_reactivation_with_personal_key(user, password)
+ personal_key = user.personal_key.gsub(/\W/, '')
+ click_on t('links.account.reactivate.with_key')
+ fill_in t('forms.personal_key.confirmation_label'), with: personal_key
+ click_on t('forms.buttons.continue')
+ fill_in t('idv.form.password'), with: password
+ click_on t('forms.buttons.continue')
+ check t('forms.personal_key.required_checkbox')
+ click_on t('forms.buttons.continue')
+ end
+
+ def login(user, password)
+ user.password = password
+ sign_in_live_with_2fa(user)
+ user.reload
+ end
+
+ def logout(user)
+ visit sign_out_url
+ user.reload
+ end
+
+ def perform_get_usps_proofing_results_job(user)
+ perform_enqueued_jobs do
+ GetUspsProofingResultsJob.new.perform(Time.zone.now)
+ end
+
+ user.reload
+ end
+
+ def stub_request_proofing_results(status, enrollment_code)
+ case status
+ when 'failed'
+ stub_request_failed_proofing_results
+ when 'cancelled'
+ stub_request_unexpected_invalid_enrollment_code(
+ { 'responseMessage' => "Enrollment code #{enrollment_code} does not exist" },
+ )
+ when 'expired'
+ stub_request_expired_id_ipp_proofing_results
+ else
+ throw "Status: #{status} not configured"
+ end
+ end
+end
diff --git a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb
index 28e4281e3ec..9456bb8a772 100644
--- a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb
+++ b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb
@@ -11,21 +11,109 @@
before do
allow(FeatureManagement).to receive(:doc_capture_polling_enabled?).and_return(true)
allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true)
- end
-
- before do
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
- shared_examples_for 'hybrid flow doc auth' do
+ it 'proofs and hands off to mobile', js: true do
+ user = nil
+
+ perform_in_browser(:desktop) do
+ visit_idp_from_sp_with_ial2(sp)
+ user = sign_up_and_2fa_ial1_user
+
+ complete_doc_auth_steps_before_hybrid_handoff_step
+ clear_and_fill_in(:doc_auth_phone, phone_number)
+ click_send_link
+
+ expect(page).to have_content(t('doc_auth.headings.text_message'))
+ expect(page).to have_content(t('doc_auth.info.you_entered'))
+ expect(page).to have_content('+1 415-555-0199')
+
+ # Confirm that Continue button is not shown when polling is enabled
+ expect(page).not_to have_content(t('doc_auth.buttons.continue'))
+ end
+
+ expect(@sms_link).to be_present
+
+ perform_in_browser(:mobile) do
+ visit @sms_link
+
+ # Confirm that jumping to LinkSent page does not cause errors
+ visit idv_link_sent_url
+ expect(page).to have_current_path(root_url)
+ visit idv_hybrid_mobile_document_capture_url
+
+ # Confirm that clicking cancel and then coming back doesn't cause errors
+ click_link 'Cancel'
+ visit idv_hybrid_mobile_document_capture_url
+
+ # Confirm that jumping to Phone page does not cause errors
+ visit idv_phone_url
+ expect(page).to have_current_path(root_url)
+ visit idv_hybrid_mobile_document_capture_url
+
+ # Confirm that jumping to Welcome page does not cause errors
+ visit idv_welcome_url
+ expect(page).to have_current_path(root_url)
+ visit idv_hybrid_mobile_document_capture_url
+
+ expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url)
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
+ attach_and_submit_images
+
+ expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
+ expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete')))
+ expect(page).to have_text(t('doc_auth.instructions.switch_back'))
+ expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
+
+ # Confirm app disallows jumping back to DocumentCapture page
+ visit idv_hybrid_mobile_document_capture_url
+ expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
+ end
+
+ perform_in_browser(:desktop) do
+ expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10)
+ expect(page).to have_current_path(idv_ssn_path)
+
+ fill_out_ssn_form_ok
+ click_idv_continue
+
+ expect(page).to have_content(t('headings.verify'))
+ complete_verify_step
+
+ prefilled_phone = page.find(id: 'idv_phone_form_phone').value
+
+ expect(
+ PhoneFormatter.format(prefilled_phone),
+ ).to eq(
+ PhoneFormatter.format(user.default_phone_configuration.phone),
+ )
+
+ fill_out_phone_form_ok
+ verify_phone_otp
+
+ fill_in t('idv.form.password'), with: Features::SessionHelper::VALID_PASSWORD
+ click_idv_continue
+
+ acknowledge_and_confirm_personal_key
+
+ validate_idv_completed_page(user)
+ click_agree_and_continue
+
+ validate_return_to_sp
+ end
+ end
+
+ context 'when facial confirmation is requested' do
it 'proofs and hands off to mobile', js: true do
user = nil
perform_in_browser(:desktop) do
- visit_idp_from_sp_with_ial2(sp)
+ visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
+
user = sign_up_and_2fa_ial1_user
complete_doc_auth_steps_before_hybrid_handoff_step
@@ -65,8 +153,8 @@
visit idv_hybrid_mobile_document_capture_url
expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url)
- expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
- attach_and_submit_images
+ attach_liveness_images
+ submit_images
expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete')))
@@ -110,101 +198,91 @@
validate_return_to_sp
end
end
+ end
- context 'when facial confirmation is requested' do
- it 'proofs and hands off to mobile', js: true do
- user = nil
+ it 'shows the waiting screen correctly after cancelling from mobile and restarting', js: true do
+ user = nil
- perform_in_browser(:desktop) do
- visit_idp_from_oidc_sp_with_ial2(facial_match_required: true)
+ perform_in_browser(:desktop) do
+ user = sign_in_and_2fa_user
+ complete_doc_auth_steps_before_hybrid_handoff_step
+ clear_and_fill_in(:doc_auth_phone, phone_number)
+ click_send_link
- user = sign_up_and_2fa_ial1_user
+ expect(page).to have_content(t('doc_auth.headings.text_message'))
+ end
- complete_doc_auth_steps_before_hybrid_handoff_step
- clear_and_fill_in(:doc_auth_phone, phone_number)
- click_send_link
+ expect(@sms_link).to be_present
- expect(page).to have_content(t('doc_auth.headings.text_message'))
- expect(page).to have_content(t('doc_auth.info.you_entered'))
- expect(page).to have_content('+1 415-555-0199')
+ perform_in_browser(:mobile) do
+ visit @sms_link
+ expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url)
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
+ click_on t('links.cancel')
+ click_on t('forms.buttons.cancel') # Yes, cancel
+ end
- # Confirm that Continue button is not shown when polling is enabled
- expect(page).not_to have_content(t('doc_auth.buttons.continue'))
- end
+ perform_in_browser(:desktop) do
+ expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10)
+ clear_and_fill_in(:doc_auth_phone, phone_number)
+ click_send_link
- expect(@sms_link).to be_present
+ expect(page).to have_content(t('doc_auth.headings.text_message'))
+ end
+ end
- perform_in_browser(:mobile) do
- visit @sms_link
+ context 'user is rate limited on mobile' do
+ let(:max_attempts) { IdentityConfig.store.doc_auth_max_attempts }
- # Confirm that jumping to LinkSent page does not cause errors
- visit idv_link_sent_url
- expect(page).to have_current_path(root_url)
- visit idv_hybrid_mobile_document_capture_url
+ before do
+ allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts)
+ DocAuth::Mock::DocAuthMockClient.mock_response!(
+ method: :post_front_image,
+ response: DocAuth::Response.new(
+ success: false,
+ errors: { network: I18n.t('doc_auth.errors.general.network_error') },
+ ),
+ )
+ end
- # Confirm that clicking cancel and then coming back doesn't cause errors
- click_link 'Cancel'
- visit idv_hybrid_mobile_document_capture_url
+ it 'shows capture complete on mobile and error page on desktop', js: true do
+ user = nil
- # Confirm that jumping to Phone page does not cause errors
- visit idv_phone_url
- expect(page).to have_current_path(root_url)
- visit idv_hybrid_mobile_document_capture_url
+ perform_in_browser(:desktop) do
+ user = sign_in_and_2fa_user
+ complete_doc_auth_steps_before_hybrid_handoff_step
+ clear_and_fill_in(:doc_auth_phone, phone_number)
+ click_send_link
- # Confirm that jumping to Welcome page does not cause errors
- visit idv_welcome_url
- expect(page).to have_current_path(root_url)
- visit idv_hybrid_mobile_document_capture_url
+ expect(page).to have_content(t('doc_auth.headings.text_message'))
+ end
- expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url)
- attach_liveness_images
- submit_images
+ expect(@sms_link).to be_present
- expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
- expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete')))
- expect(page).to have_text(t('doc_auth.instructions.switch_back'))
- expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id'))
+ perform_in_browser(:mobile) do
+ visit @sms_link
- # Confirm app disallows jumping back to DocumentCapture page
- visit idv_hybrid_mobile_document_capture_url
- expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
+ (max_attempts - 1).times do
+ attach_and_submit_images
+ click_on t('idv.failure.button.warning')
end
- perform_in_browser(:desktop) do
- expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10)
- expect(page).to have_current_path(idv_ssn_path)
-
- fill_out_ssn_form_ok
- click_idv_continue
-
- expect(page).to have_content(t('headings.verify'))
- complete_verify_step
-
- prefilled_phone = page.find(id: 'idv_phone_form_phone').value
-
- expect(
- PhoneFormatter.format(prefilled_phone),
- ).to eq(
- PhoneFormatter.format(user.default_phone_configuration.phone),
- )
-
- fill_out_phone_form_ok
- verify_phone_otp
-
- fill_in t('idv.form.password'), with: Features::SessionHelper::VALID_PASSWORD
- click_idv_continue
-
- acknowledge_and_confirm_personal_key
+ # final failure
+ attach_and_submit_images
- validate_idv_completed_page(user)
- click_agree_and_continue
+ expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
+ expect(page).not_to have_content(strip_nbsp(t('doc_auth.headings.capture_complete')))
+ expect(page).to have_text(t('doc_auth.instructions.switch_back'))
+ end
- validate_return_to_sp
- end
+ perform_in_browser(:desktop) do
+ expect(page).to have_current_path(idv_session_errors_rate_limited_path, wait: 10)
end
end
+ end
- it 'shows the waiting screen correctly after cancelling from mobile and restarting', js: true do
+ context 'barcode read error on mobile (redo document capture)' do
+ it 'continues to ssn on desktop when user selects Continue', js: true do
user = nil
perform_in_browser(:desktop) do
@@ -220,266 +298,173 @@
perform_in_browser(:mobile) do
visit @sms_link
- expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url)
- expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
- click_on t('links.cancel')
- click_on t('forms.buttons.cancel') # Yes, cancel
- end
-
- perform_in_browser(:desktop) do
- expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10)
- clear_and_fill_in(:doc_auth_phone, phone_number)
- click_send_link
- expect(page).to have_content(t('doc_auth.headings.text_message'))
- end
- end
+ mock_doc_auth_attention_with_barcode
+ attach_and_submit_images
+ click_idv_continue
- context 'user is rate limited on mobile' do
- let(:max_attempts) { IdentityConfig.store.doc_auth_max_attempts }
-
- before do
- allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts)
- DocAuth::Mock::DocAuthMockClient.mock_response!(
- method: :post_front_image,
- response: DocAuth::Response.new(
- success: false,
- errors: { network: I18n.t('doc_auth.errors.general.network_error') },
- ),
- )
+ expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
+ expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete')))
+ expect(page).to have_text(t('doc_auth.instructions.switch_back'))
end
- it 'shows capture complete on mobile and error page on desktop', js: true do
- user = nil
-
- perform_in_browser(:desktop) do
- user = sign_in_and_2fa_user
- complete_doc_auth_steps_before_hybrid_handoff_step
- clear_and_fill_in(:doc_auth_phone, phone_number)
- click_send_link
-
- expect(page).to have_content(t('doc_auth.headings.text_message'))
- end
-
- expect(@sms_link).to be_present
+ perform_in_browser(:desktop) do
+ expect(page).to have_current_path(idv_ssn_path, wait: 10)
- perform_in_browser(:mobile) do
- visit @sms_link
+ fill_out_ssn_form_ok
+ click_idv_continue
- (max_attempts - 1).times do
- attach_and_submit_images
- click_on t('idv.failure.button.warning')
- end
+ expect(page).to have_current_path(idv_verify_info_path, wait: 10)
- # final failure
- attach_and_submit_images
+ # verify pii is displayed
+ expect(page).to have_text('DAVID')
+ expect(page).to have_text('SAMPLE')
+ expect(page).to have_text('123 ABC AVE')
- expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
- expect(page).not_to have_content(strip_nbsp(t('doc_auth.headings.capture_complete')))
- expect(page).to have_text(t('doc_auth.instructions.switch_back'))
- end
+ warning_link_text = t('doc_auth.headings.capture_scan_warning_link')
+ click_link warning_link_text
- perform_in_browser(:desktop) do
- expect(page).to have_current_path(idv_session_errors_rate_limited_path, wait: 10)
- end
+ expect(current_path).to eq(idv_hybrid_handoff_path)
+ clear_and_fill_in(:doc_auth_phone, phone_number)
+ click_send_link
end
- end
-
- context 'barcode read error on mobile (redo document capture)' do
- it 'continues to ssn on desktop when user selects Continue', js: true do
- user = nil
-
- perform_in_browser(:desktop) do
- user = sign_in_and_2fa_user
- complete_doc_auth_steps_before_hybrid_handoff_step
- clear_and_fill_in(:doc_auth_phone, phone_number)
- click_send_link
-
- expect(page).to have_content(t('doc_auth.headings.text_message'))
- end
-
- expect(@sms_link).to be_present
-
- perform_in_browser(:mobile) do
- visit @sms_link
- mock_doc_auth_attention_with_barcode
- attach_and_submit_images
- click_idv_continue
-
- expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
- expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete')))
- expect(page).to have_text(t('doc_auth.instructions.switch_back'))
- end
-
- perform_in_browser(:desktop) do
- expect(page).to have_current_path(idv_ssn_path, wait: 10)
-
- fill_out_ssn_form_ok
- click_idv_continue
-
- expect(page).to have_current_path(idv_verify_info_path, wait: 10)
-
- # verify pii is displayed
- expect(page).to have_text('DAVID')
- expect(page).to have_text('SAMPLE')
- expect(page).to have_text('123 ABC AVE')
-
- warning_link_text = t('doc_auth.headings.capture_scan_warning_link')
- click_link warning_link_text
-
- expect(current_path).to eq(idv_hybrid_handoff_path)
- clear_and_fill_in(:doc_auth_phone, phone_number)
- click_send_link
- end
+ perform_in_browser(:mobile) do
+ visit @sms_link
- perform_in_browser(:mobile) do
- visit @sms_link
+ DocAuth::Mock::DocAuthMockClient.reset!
+ attach_and_submit_images
- DocAuth::Mock::DocAuthMockClient.reset!
- attach_and_submit_images
+ expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
+ end
- expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
- end
+ perform_in_browser(:desktop) do
+ expect(page).to have_current_path(idv_ssn_path, wait: 10)
+ complete_ssn_step
+ expect(page).to have_current_path(idv_verify_info_path)
+
+ # verify orig pii no longer displayed
+ expect(page).not_to have_text('DAVID')
+ expect(page).not_to have_text('SAMPLE')
+ expect(page).not_to have_text('123 ABC AVE')
+ # verify new pii from redo is displayed
+ expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:first_name])
+ expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:last_name])
+ expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:address1])
- perform_in_browser(:desktop) do
- expect(page).to have_current_path(idv_ssn_path, wait: 10)
- complete_ssn_step
- expect(page).to have_current_path(idv_verify_info_path)
-
- # verify orig pii no longer displayed
- expect(page).not_to have_text('DAVID')
- expect(page).not_to have_text('SAMPLE')
- expect(page).not_to have_text('123 ABC AVE')
- # verify new pii from redo is displayed
- expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:first_name])
- expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:last_name])
- expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:address1])
-
- complete_verify_step
- end
+ complete_verify_step
end
end
+ end
- context 'barcode read error on desktop, redo document capture on mobile' do
- it 'continues to ssn on desktop when user selects Continue', js: true do
- user = nil
-
- perform_in_browser(:desktop) do
- user = sign_in_and_2fa_user
- complete_doc_auth_steps_before_document_capture_step
- mock_doc_auth_attention_with_barcode
- attach_and_submit_images
- click_idv_continue
- expect(page).to have_current_path(idv_ssn_path, wait: 10)
-
- fill_out_ssn_form_ok
- click_idv_continue
-
- expect(page).to have_current_path(idv_verify_info_path, wait: 10)
-
- # verify pii is displayed
- expect(page).to have_text('DAVID')
- expect(page).to have_text('SAMPLE')
- expect(page).to have_text('123 ABC AVE')
-
- warning_link_text = t('doc_auth.headings.capture_scan_warning_link')
- click_link warning_link_text
-
- expect(current_path).to eq(idv_hybrid_handoff_path)
- clear_and_fill_in(:doc_auth_phone, phone_number)
- click_send_link
- end
-
- perform_in_browser(:mobile) do
- visit @sms_link
-
- DocAuth::Mock::DocAuthMockClient.reset!
-
- expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url)
- expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
-
- visit(idv_hybrid_mobile_document_capture_url(selfie: true))
- expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url(selfie: true))
- expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
+ context 'barcode read error on desktop, redo document capture on mobile' do
+ it 'continues to ssn on desktop when user selects Continue', js: true do
+ user = nil
- attach_and_submit_images
+ perform_in_browser(:desktop) do
+ user = sign_in_and_2fa_user
+ complete_doc_auth_steps_before_document_capture_step
+ mock_doc_auth_attention_with_barcode
+ attach_and_submit_images
+ click_idv_continue
+ expect(page).to have_current_path(idv_ssn_path, wait: 10)
- expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
- end
+ fill_out_ssn_form_ok
+ click_idv_continue
- perform_in_browser(:desktop) do
- expect(page).to have_current_path(idv_ssn_path, wait: 10)
- complete_ssn_step
- expect(page).to have_current_path(idv_verify_info_path)
-
- # verify orig pii no longer displayed
- expect(page).not_to have_text('DAVID')
- expect(page).not_to have_text('SAMPLE')
- expect(page).not_to have_text('123 ABC AVE')
- # verify new pii from redo is displayed
- expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:first_name])
- expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:last_name])
- expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:address1])
-
- complete_verify_step
- end
- end
- end
+ expect(page).to have_current_path(idv_verify_info_path, wait: 10)
- it 'prefills the phone number used on the phone step if the user has no MFA phone', :js do
- user = create(:user, :with_authentication_app)
+ # verify pii is displayed
+ expect(page).to have_text('DAVID')
+ expect(page).to have_text('SAMPLE')
+ expect(page).to have_text('123 ABC AVE')
- perform_in_browser(:desktop) do
- start_idv_from_sp(facial_match_required: true)
- sign_in_and_2fa_user(user)
+ warning_link_text = t('doc_auth.headings.capture_scan_warning_link')
+ click_link warning_link_text
- complete_doc_auth_steps_before_hybrid_handoff_step
+ expect(current_path).to eq(idv_hybrid_handoff_path)
clear_and_fill_in(:doc_auth_phone, phone_number)
click_send_link
end
- expect(@sms_link).to be_present
-
perform_in_browser(:mobile) do
visit @sms_link
+ DocAuth::Mock::DocAuthMockClient.reset!
+
expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url)
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
- attach_liveness_images
- submit_images
+ visit(idv_hybrid_mobile_document_capture_url(selfie: true))
+ expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url(selfie: true))
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
+
+ attach_and_submit_images
expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
- expect(page).to have_text(t('doc_auth.instructions.switch_back'))
end
perform_in_browser(:desktop) do
expect(page).to have_current_path(idv_ssn_path, wait: 10)
+ complete_ssn_step
+ expect(page).to have_current_path(idv_verify_info_path)
+
+ # verify orig pii no longer displayed
+ expect(page).not_to have_text('DAVID')
+ expect(page).not_to have_text('SAMPLE')
+ expect(page).not_to have_text('123 ABC AVE')
+ # verify new pii from redo is displayed
+ expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:first_name])
+ expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:last_name])
+ expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:address1])
- fill_out_ssn_form_ok
- click_idv_continue
-
- expect(page).to have_content(t('headings.verify'))
complete_verify_step
-
- prefilled_phone = page.find(id: 'idv_phone_form_phone').value
-
- expect(
- PhoneFormatter.format(prefilled_phone),
- ).to eq(
- PhoneFormatter.format(phone_number),
- )
end
end
end
- it_behaves_like 'hybrid flow doc auth'
+ it 'prefills the phone number used on the phone step if the user has no MFA phone', :js do
+ user = create(:user, :with_authentication_app)
- context 'split doc auth flow' do
- before do
- allow(IdentityConfig.store).to receive(:doc_auth_separate_pages_enabled).and_return(true)
+ perform_in_browser(:desktop) do
+ start_idv_from_sp(facial_match_required: true)
+ sign_in_and_2fa_user(user)
+
+ complete_doc_auth_steps_before_hybrid_handoff_step
+ clear_and_fill_in(:doc_auth_phone, phone_number)
+ click_send_link
end
- it_behaves_like 'hybrid flow doc auth'
+ expect(@sms_link).to be_present
+
+ perform_in_browser(:mobile) do
+ visit @sms_link
+
+ expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url)
+
+ attach_liveness_images
+ submit_images
+
+ expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url)
+ expect(page).to have_text(t('doc_auth.instructions.switch_back'))
+ end
+
+ perform_in_browser(:desktop) do
+ expect(page).to have_current_path(idv_ssn_path, wait: 10)
+
+ fill_out_ssn_form_ok
+ click_idv_continue
+
+ expect(page).to have_content(t('headings.verify'))
+ complete_verify_step
+
+ prefilled_phone = page.find(id: 'idv_phone_form_phone').value
+
+ expect(
+ PhoneFormatter.format(prefilled_phone),
+ ).to eq(
+ PhoneFormatter.format(phone_number),
+ )
+ end
end
end
diff --git a/spec/features/multiple_emails/sp_sign_in_spec.rb b/spec/features/multiple_emails/sp_sign_in_spec.rb
index be6d75b55b6..33b0a5fc6f8 100644
--- a/spec/features/multiple_emails/sp_sign_in_spec.rb
+++ b/spec/features/multiple_emails/sp_sign_in_spec.rb
@@ -17,9 +17,8 @@
fill_in_code_with_last_phone_otp
click_submit_default
click_agree_and_continue if current_path == sign_up_completed_path
- decoded_id_token = fetch_oidc_id_token_info
- expect(decoded_id_token[:email]).to eq(emails.first)
- expect(decoded_id_token[:all_emails]).to be_nil
+ expect(oidc_decoded_id_token[:email]).to eq(emails.first)
+ expect(oidc_decoded_id_token[:all_emails]).to be_nil
Capybara.reset_session!
end
@@ -41,8 +40,7 @@
expect(current_path).to eq(sign_up_completed_path)
click_agree_and_continue
- decoded_id_token = fetch_oidc_id_token_info
- expect(decoded_id_token[:email]).to eq(emails.second)
+ expect(oidc_decoded_id_token[:email]).to eq(emails.second)
end
scenario 'signing in with OIDC after deleting email linked to identity' do
@@ -69,8 +67,7 @@
# Sign in again to partner application
visit_idp_from_oidc_sp(scope: 'openid email')
- decoded_id_token = fetch_oidc_id_token_info
- expect(decoded_id_token[:email]).to eq(email1.email)
+ expect(oidc_decoded_id_token[:email]).to eq(email1.email)
end
scenario 'signing in with SAML sends the email address used to sign in' do
@@ -161,8 +158,7 @@
click_submit_default
click_agree_and_continue
- decoded_id_token = fetch_oidc_id_token_info
- expect(decoded_id_token[:all_emails]).to match_array(emails)
+ expect(oidc_decoded_id_token[:all_emails]).to match_array(emails)
end
scenario 'signing in with SAML sends all emails' do
@@ -206,41 +202,4 @@ def visit_idp_from_oidc_sp(scope:)
nonce: SecureRandom.hex,
)
end
-
- def fetch_oidc_id_token_info
- redirect_uri = URI(oidc_redirect_url)
- redirect_params = Rack::Utils.parse_query(redirect_uri.query).with_indifferent_access
- code = redirect_params[:code]
-
- jwt_payload = {
- iss: 'urn:gov:gsa:openidconnect:sp:server',
- sub: 'urn:gov:gsa:openidconnect:sp:server',
- aud: api_openid_connect_token_url,
- jti: SecureRandom.hex,
- exp: 5.minutes.from_now.to_i,
- }
-
- client_assertion = JWT.encode(jwt_payload, client_private_key, 'RS256')
- client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
-
- page.driver.post(
- api_openid_connect_token_path,
- grant_type: 'authorization_code',
- code: code,
- client_assertion_type: client_assertion_type,
- client_assertion: client_assertion,
- )
-
- token_response = JSON.parse(page.body).with_indifferent_access
- id_token = token_response[:id_token]
- JWT.decode(id_token, nil, false).first.with_indifferent_access
- end
-
- def client_private_key
- @client_private_key ||= begin
- OpenSSL::PKey::RSA.new(
- File.read(Rails.root.join('keys', 'saml_test_sp.key')),
- )
- end
- end
end
diff --git a/spec/features/openid_connect/phishing_resistant_required_spec.rb b/spec/features/openid_connect/phishing_resistant_required_spec.rb
index 45ed9cb2fd8..b3664efeccc 100644
--- a/spec/features/openid_connect/phishing_resistant_required_spec.rb
+++ b/spec/features/openid_connect/phishing_resistant_required_spec.rb
@@ -60,36 +60,42 @@
context 'with piv cac configured' do
let(:user) { create(:user, :fully_registered, :with_piv_or_cac) }
- it 'sends user to authenticate with piv cac' do
+ it 'sends user to authenticate with piv cac and removes weaker options' do
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_requesting_aal3(prompt: 'select_account')
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_piv_cac_url)
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:piv_cac)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
context 'with webauthn configured' do
let(:user) { create(:user, :fully_registered, :with_webauthn) }
- it 'sends user to authenticate with webauthn' do
+ it 'sends user to authenticate with webauthn and removes weaker options' do
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_requesting_aal3(prompt: 'select_account')
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_webauthn_url)
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:webauthn)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
context 'with webauthn platform configured' do
let(:user) { create(:user, :fully_registered, :with_webauthn_platform) }
- it 'sends user to authenticate with webauthn platform' do
+ it 'sends user to authenticate with webauthn platform and removes weaker options' do
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_requesting_aal3(prompt: 'select_account')
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:webauthn_platform)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
@@ -132,36 +138,42 @@
context 'with piv cac configured' do
let(:user) { create(:user, :fully_registered, :with_piv_or_cac) }
- it 'sends user to authenticate with piv cac' do
+ it 'sends user to authenticate with piv cac and removes weaker options' do
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_requesting_phishing_resistant(prompt: 'select_account')
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_piv_cac_url)
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:piv_cac)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
context 'with webauthn configured' do
let(:user) { create(:user, :fully_registered, :with_webauthn) }
- it 'sends user to authenticate with webauthn' do
+ it 'sends user to authenticate with webauthn and removes weaker options' do
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_requesting_phishing_resistant(prompt: 'select_account')
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_webauthn_url)
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:webauthn)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
context 'with webauthn platform configured' do
let(:user) { create(:user, :fully_registered, :with_webauthn_platform) }
- it 'sends user to authenticate with webauthn platform' do
+ it 'sends user to authenticate with webauthn platform and removes weaker options' do
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_requesting_phishing_resistant(prompt: 'select_account')
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:webauthn_platform)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
@@ -204,36 +216,42 @@
context 'with piv cac configured' do
let(:user) { create(:user, :fully_registered, :with_piv_or_cac) }
- it 'sends user to authenticate with piv cac' do
+ it 'sends user to authenticate with piv cac and removes weaker options' do
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_defaulting_to_aal3(prompt: 'select_account')
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_piv_cac_url)
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:piv_cac)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
context 'with webauthn configured' do
let(:user) { create(:user, :fully_registered, :with_webauthn) }
- it 'sends user to authenticate with webauthn' do
+ it 'sends user to authenticate with webauthn and removes weaker options' do
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_defaulting_to_aal3(prompt: 'select_account')
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_webauthn_url)
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:webauthn)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
context 'with webauthn platform configured' do
let(:user) { create(:user, :fully_registered, :with_webauthn_platform) }
- it 'sends user to authenticate with webauthn platform' do
+ it 'sends user to authenticate with webauthn platform and removes weaker options' do
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_defaulting_to_aal3(prompt: 'select_account')
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:webauthn_platform)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
@@ -262,4 +280,11 @@
end
end
end
+
+ def has_2fa_option?(auth_method)
+ page.find("label[for='two_factor_options_form_selection_#{auth_method}']")
+ true
+ rescue Capybara::ElementNotFound
+ false
+ end
end
diff --git a/spec/features/openid_connect/vtr_spec.rb b/spec/features/openid_connect/vtr_spec.rb
index 06683ad5da2..95b8281afbb 100644
--- a/spec/features/openid_connect/vtr_spec.rb
+++ b/spec/features/openid_connect/vtr_spec.rb
@@ -128,6 +128,6 @@
click_button(t('doc_auth.buttons.upload_picture'))
- expect(page).to have_content(t('doc_auth.headings.document_capture_subheader_selfie'))
+ expect(page).to have_content(t('doc_auth.headings.document_capture'))
end
end
diff --git a/spec/features/saml/phishing_resistant_required_spec.rb b/spec/features/saml/phishing_resistant_required_spec.rb
index 329dddd5709..772a8b4b824 100644
--- a/spec/features/saml/phishing_resistant_required_spec.rb
+++ b/spec/features/saml/phishing_resistant_required_spec.rb
@@ -66,7 +66,7 @@
context 'with piv cac configured' do
let(:user) { create(:user, :fully_registered, :with_piv_or_cac) }
- it 'sends user to authenticate with piv cac' do
+ it 'sends user to authenticate with piv cac and removes weaker options' do
sign_in_before_2fa(user)
visit_saml_authn_request_url(
@@ -75,15 +75,17 @@
authn_context: Saml::Idp::Constants::AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF,
},
)
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_piv_cac_url)
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:piv_cac)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
context 'with webauthn configured' do
let(:user) { create(:user, :fully_registered, :with_webauthn) }
- it 'sends user to authenticate with webauthn' do
+ it 'sends user to authenticate with webauthn and removes weaker options' do
sign_in_before_2fa(user)
visit_saml_authn_request_url(
@@ -92,15 +94,17 @@
authn_context: Saml::Idp::Constants::AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF,
},
)
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_webauthn_url)
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:webauthn)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
context 'with webauthn platform configured' do
let(:user) { create(:user, :fully_registered, :with_webauthn_platform) }
- it 'sends user to authenticate with webauthn platform' do
+ it 'sends user to authenticate with webauthn platform and removes weaker options' do
sign_in_before_2fa(user)
visit_saml_authn_request_url(
@@ -109,8 +113,10 @@
authn_context: Saml::Idp::Constants::AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF,
},
)
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:webauthn_platform)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
@@ -157,7 +163,7 @@
context 'with piv cac configured' do
let(:user) { create(:user, :fully_registered, :with_piv_or_cac) }
- it 'sends user to authenticate with piv cac' do
+ it 'sends user to authenticate with piv cac and removes weaker options' do
sign_in_before_2fa(user)
visit_saml_authn_request_url(
@@ -165,15 +171,17 @@
issuer: sp1_issuer, authn_context: Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF
},
)
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_piv_cac_url)
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:piv_cac)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
context 'with webauthn configured' do
let(:user) { create(:user, :fully_registered, :with_webauthn) }
- it 'sends user to authenticate with webauthn' do
+ it 'sends user to authenticate with webauthn and removes weaker options' do
sign_in_before_2fa(user)
visit_saml_authn_request_url(
@@ -181,15 +189,17 @@
issuer: sp1_issuer, authn_context: Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF
},
)
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_webauthn_url)
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:webauthn)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
context 'with webauthn platform configured' do
let(:user) { create(:user, :fully_registered, :with_webauthn_platform) }
- it 'sends user to authenticate with webauthn platform' do
+ it 'sends user to authenticate with webauthn platform and removes weaker options' do
sign_in_before_2fa(user)
visit_saml_authn_request_url(
@@ -197,8 +207,10 @@
issuer: sp1_issuer, authn_context: Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF
},
)
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:webauthn_platform)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
@@ -245,7 +257,7 @@
context 'with piv cac configured' do
let(:user) { create(:user, :fully_registered, :with_piv_or_cac) }
- it 'sends user to authenticate with piv cac' do
+ it 'sends user to authenticate with piv cac and removes weaker options' do
sign_in_before_2fa(user)
visit_saml_authn_request_url(
@@ -253,15 +265,17 @@
issuer: aal3_issuer, authn_context: nil
},
)
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_piv_cac_url)
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:piv_cac)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
context 'with webauthn configured' do
let(:user) { create(:user, :fully_registered, :with_webauthn) }
- it 'sends user to authenticate with webauthn' do
+ it 'sends user to authenticate with webauthn and removes weaker options' do
sign_in_before_2fa(user)
visit_saml_authn_request_url(
@@ -269,15 +283,17 @@
issuer: aal3_issuer, authn_context: nil
},
)
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_webauthn_url)
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:webauthn)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
context 'with webauthn platform configured' do
let(:user) { create(:user, :fully_registered, :with_webauthn_platform) }
- it 'sends user to authenticate with webauthn platform' do
+ it 'sends user to authenticate with webauthn platform and removes weaker options' do
sign_in_before_2fa(user)
visit_saml_authn_request_url(
@@ -285,8 +301,10 @@
issuer: aal3_issuer, authn_context: nil
},
)
- visit login_two_factor_path(otp_delivery_preference: 'sms')
expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ click_on t('two_factor_authentication.login_options_link_text')
+ expect(has_2fa_option?(:webauthn_platform)).to eq(true)
+ expect(has_2fa_option?(:sms)).to eq(false)
end
end
@@ -324,4 +342,11 @@
end
end
end
+
+ def has_2fa_option?(auth_method)
+ page.find("label[for='two_factor_options_form_selection_#{auth_method}']")
+ true
+ rescue Capybara::ElementNotFound
+ false
+ end
end
diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb
index 0aff958e708..297a23f5efe 100644
--- a/spec/features/users/sign_in_spec.rb
+++ b/spec/features/users/sign_in_spec.rb
@@ -879,12 +879,14 @@
end
end
- context 'reCAPTCHA check fails' do
+ context 'when the reCAPTCHA check fails' do
let(:user) { create(:user, :fully_registered) }
before do
allow(FeatureManagement).to receive(:sign_in_recaptcha_enabled?).and_return(true)
allow(IdentityConfig.store).to receive(:recaptcha_mock_validator).and_return(true)
+ allow(IdentityConfig.store).to receive(:sign_in_recaptcha_log_failures_only).
+ and_return(sign_in_recaptcha_log_failures_only)
allow(IdentityConfig.store).to receive(:sign_in_recaptcha_score_threshold).and_return(0.2)
allow(IdentityConfig.store).to receive(:sign_in_recaptcha_percent_tested).and_return(100)
reload_ab_tests
@@ -894,40 +896,16 @@
reload_ab_tests
end
- it 'redirects user to security check failed page' do
- visit new_user_session_path
-
- asserted_expected_user = false
- fake_analytics = FakeAnalytics.new
- allow_any_instance_of(ApplicationController).to receive(:analytics).
- and_wrap_original do |original|
- original_analytics = original.call
- if original_analytics.request.params[:controller] == 'users/sessions' &&
- original_analytics.request.params[:action] == 'create'
- expect(original_analytics.user).to eq(user)
- asserted_expected_user = true
- end
-
- fake_analytics
- end
+ context 'when configured to log failures only' do
+ let(:sign_in_recaptcha_log_failures_only) { true }
+ it_behaves_like 'logs reCAPTCHA event and redirects appropriately',
+ successful_sign_in: true
+ end
- fill_in :user_recaptcha_mock_score, with: '0.1'
- fill_in_credentials_and_submit(user.email, user.password)
- expect(asserted_expected_user).to eq(true)
- expect(fake_analytics).to have_logged_event(
- 'reCAPTCHA verify result received',
- recaptcha_result: {
- assessment_id: kind_of(String),
- success: true,
- score: 0.1,
- errors: [],
- reasons: [],
- },
- evaluated_as_valid: false,
- score_threshold: 0.2,
- form_class: 'RecaptchaMockForm',
- )
- expect(current_path).to eq sign_in_security_check_failed_path
+ context 'when not configured to log failures only' do
+ let(:sign_in_recaptcha_log_failures_only) { false }
+ it_behaves_like 'logs reCAPTCHA event and redirects appropriately',
+ successful_sign_in: false
end
end
diff --git a/spec/javascript/packages/document-capture/components/document-capture-spec.jsx b/spec/javascript/packages/document-capture/components/document-capture-spec.jsx
index 2b5c3f4b15b..356b4c54d85 100644
--- a/spec/javascript/packages/document-capture/components/document-capture-spec.jsx
+++ b/spec/javascript/packages/document-capture/components/document-capture-spec.jsx
@@ -372,9 +372,7 @@ describe('document-capture/components/document-capture', () => {
it('does not show selfie on first page when doc auth seperated pages enabled', () => {
const { queryByText } = render(
-
+
,
);
diff --git a/spec/javascript/packages/document-capture/components/document-side-acuant-capture-spec.tsx b/spec/javascript/packages/document-capture/components/document-side-acuant-capture-spec.tsx
index a22d1deaa24..f45e22de68e 100644
--- a/spec/javascript/packages/document-capture/components/document-side-acuant-capture-spec.tsx
+++ b/spec/javascript/packages/document-capture/components/document-side-acuant-capture-spec.tsx
@@ -23,7 +23,8 @@ describe('DocumentSideAcuantCapture', () => {
value={{
isSelfieCaptureEnabled: false,
isSelfieDesktopTestMode: false,
- docAuthSeparatePagesEnabled: false,
+ showHelpInitially: false,
+ immediatelyBeginCapture: false,
}}
>
@@ -47,7 +48,8 @@ describe('DocumentSideAcuantCapture', () => {
value={{
isSelfieCaptureEnabled: false,
isSelfieDesktopTestMode: true,
- docAuthSeparatePagesEnabled: false,
+ showHelpInitially: false,
+ immediatelyBeginCapture: false,
}}
>
@@ -73,7 +75,8 @@ describe('DocumentSideAcuantCapture', () => {
value={{
isSelfieCaptureEnabled: false,
isSelfieDesktopTestMode: false,
- docAuthSeparatePagesEnabled: false,
+ showHelpInitially: false,
+ immediatelyBeginCapture: false,
}}
>
@@ -95,7 +98,8 @@ describe('DocumentSideAcuantCapture', () => {
value={{
isSelfieCaptureEnabled: false,
isSelfieDesktopTestMode: true,
- docAuthSeparatePagesEnabled: false,
+ showHelpInitially: false,
+ immediatelyBeginCapture: false,
}}
>
@@ -121,7 +125,8 @@ describe('DocumentSideAcuantCapture', () => {
value={{
isSelfieCaptureEnabled: true,
isSelfieDesktopTestMode: false,
- docAuthSeparatePagesEnabled: false,
+ showHelpInitially: false,
+ immediatelyBeginCapture: false,
}}
>
@@ -149,7 +154,8 @@ describe('DocumentSideAcuantCapture', () => {
value={{
isSelfieCaptureEnabled: true,
isSelfieDesktopTestMode: true,
- docAuthSeparatePagesEnabled: false,
+ showHelpInitially: false,
+ immediatelyBeginCapture: false,
}}
>
@@ -185,7 +191,8 @@ describe('DocumentSideAcuantCapture', () => {
value={{
isSelfieCaptureEnabled: true,
isSelfieDesktopTestMode: true,
- docAuthSeparatePagesEnabled: false,
+ showHelpInitially: false,
+ immediatelyBeginCapture: false,
}}
>
diff --git a/spec/javascript/packages/document-capture/components/documents-and-selfie-step-spec.tsx b/spec/javascript/packages/document-capture/components/documents-step-spec.tsx
similarity index 62%
rename from spec/javascript/packages/document-capture/components/documents-and-selfie-step-spec.tsx
rename to spec/javascript/packages/document-capture/components/documents-step-spec.tsx
index a344d3198be..80bedd1160f 100644
--- a/spec/javascript/packages/document-capture/components/documents-and-selfie-step-spec.tsx
+++ b/spec/javascript/packages/document-capture/components/documents-step-spec.tsx
@@ -1,5 +1,4 @@
import userEvent from '@testing-library/user-event';
-import { within } from '@testing-library/react';
import sinon from 'sinon';
import { expect } from 'chai';
import { t } from '@18f/identity-i18n';
@@ -9,15 +8,14 @@ import {
FailedCaptureAttemptsContextProvider,
SelfieCaptureContext,
} from '@18f/identity-document-capture';
-import DocumentsAndSelfieStep from '@18f/identity-document-capture/components/documents-and-selfie-step';
-import { composeComponents } from '@18f/identity-compose-components';
+import DocumentsStep from '@18f/identity-document-capture/components/documents-step';
import { render } from '../../../support/document-capture';
import { getFixtureFile } from '../../../support/file';
-describe('document-capture/components/documents-and-selfie-step', () => {
+describe('document-capture/components/documents-step', () => {
it('renders with only front and back inputs by default', () => {
const { getByLabelText, queryByLabelText } = render(
- undefined}
errors={[]}
@@ -45,7 +43,7 @@ describe('document-capture/components/documents-and-selfie-step', () => {
maxSubmissionAttemptsBeforeNativeCamera={3}
failedFingerprints={{ front: [], back: [] }}
>
- {
it('renders device-specific instructions', () => {
let { getByText } = render(
- undefined}
errors={[]}
@@ -87,7 +85,7 @@ describe('document-capture/components/documents-and-selfie-step', () => {
expect(() => getByText('doc_auth.tips.document_capture_id_text4')).to.throw();
getByText = render(
- undefined}
errors={[]}
@@ -105,7 +103,7 @@ describe('document-capture/components/documents-and-selfie-step', () => {
const { getByText } = render(
- undefined}
errors={[]}
@@ -126,7 +124,7 @@ describe('document-capture/components/documents-and-selfie-step', () => {
const { queryByText } = render(
- undefined}
errors={[]}
@@ -143,70 +141,27 @@ describe('document-capture/components/documents-and-selfie-step', () => {
expect(queryByText(notExpectedText)).to.not.exist();
});
- context('selfie capture', () => {
- it('renders with front, back, and selfie inputs when isSelfieCaptureEnabled is true', () => {
- const App = composeComponents(
- [
- SelfieCaptureContext.Provider,
- {
- value: {
- isSelfieCaptureEnabled: true,
- },
- },
- ],
- [DocumentsAndSelfieStep],
- );
- const { getAllByRole, getByText, getByRole, getByLabelText, queryByLabelText } = render(
- ,
- );
-
- const front = getByLabelText('doc_auth.headings.document_capture_front');
- const back = getByLabelText('doc_auth.headings.document_capture_back');
- const selfie = queryByLabelText('doc_auth.headings.document_capture_selfie');
- const pageHeader = getByRole('heading', {
- name: 'doc_auth.headings.document_capture_with_selfie',
- level: 1,
- });
- const idHeader = getByRole('heading', {
- name: '1. doc_auth.headings.document_capture_subheader_id',
- level: 2,
- });
- const selfieHeader = getByRole('heading', {
- name: 'doc_auth.headings.document_capture_subheader_selfie',
- level: 1,
- });
- expect(front).to.be.ok();
- expect(back).to.be.ok();
- expect(selfie).to.be.ok();
- expect(pageHeader).to.be.ok();
- expect(idHeader).to.be.ok();
- expect(selfieHeader).to.be.ok();
-
- const tipListHeader = getByText('doc_auth.tips.document_capture_selfie_selfie_text');
- expect(tipListHeader).to.be.ok();
- const lists = getAllByRole('list');
- const tipList = lists[1];
- expect(tipList).to.be.ok();
- const tipListItem = within(tipList).getAllByRole('listitem');
- tipListItem.forEach((li, idx) => {
- expect(li.textContent).to.equals(`doc_auth.tips.document_capture_selfie_text${idx + 1}`);
- });
- });
- });
-
- it('renders with front, back when isSelfieCaptureEnabled is false', () => {
- const App = composeComponents(
- [
- SelfieCaptureContext.Provider,
- {
- value: {
- isSelfieCaptureEnabled: false,
- },
- },
- ],
- [DocumentsAndSelfieStep],
+ it('renders only with front, back when isSelfieCaptureEnabled is true', () => {
+ const { getByRole, getByLabelText } = render(
+
+ undefined}
+ errors={[]}
+ onError={() => undefined}
+ registerField={() => undefined}
+ unknownFieldErrors={[]}
+ toPreviousStep={() => undefined}
+ />
+ ,
);
- const { queryByRole, getByRole, getByLabelText } = render( );
const front = getByLabelText('doc_auth.headings.document_capture_front');
const back = getByLabelText('doc_auth.headings.document_capture_back');
@@ -214,14 +169,9 @@ describe('document-capture/components/documents-and-selfie-step', () => {
name: 'doc_auth.headings.document_capture',
level: 1,
});
- const idHeader = queryByRole('heading', {
- name: 'doc_auth.headings.document_capture_subheader_id',
- level: 2,
- });
expect(front).to.be.ok();
expect(back).to.be.ok();
expect(pageHeader).to.be.ok();
- expect(idHeader).to.be.not.ok();
});
});
diff --git a/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx b/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx
index 08910f65fd4..986c46446b3 100644
--- a/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx
+++ b/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx
@@ -11,7 +11,6 @@ import { I18n } from '@18f/identity-i18n';
import { I18nContext } from '@18f/identity-react-i18n';
import ReviewIssuesStep from '@18f/identity-document-capture/components/review-issues-step';
import { toFormEntryError } from '@18f/identity-document-capture/services/upload';
-import { composeComponents } from '@18f/identity-compose-components';
import { render } from '../../../support/document-capture';
import { getFixtureFile } from '../../../support/file';
@@ -180,18 +179,11 @@ describe('document-capture/components/review-issues-step', () => {
});
it('renders with front, back, and selfie inputs when isSelfieCaptureEnabled is true', async () => {
- const App = composeComponents(
- [
- SelfieCaptureContext.Provider,
- {
- value: {
- isSelfieCaptureEnabled: true,
- },
- },
- ],
- [ReviewIssuesStep, DEFAULT_PROPS],
+ const { getByLabelText, queryByLabelText, getByRole } = render(
+
+
+ ,
);
- const { getByLabelText, queryByLabelText, getByRole } = render( );
await userEvent.click(getByRole('button', { name: 'idv.failure.button.warning' }));
diff --git a/spec/javascript/packages/document-capture/components/selfie-step-spec.tsx b/spec/javascript/packages/document-capture/components/selfie-step-spec.tsx
new file mode 100644
index 00000000000..0fec529a12d
--- /dev/null
+++ b/spec/javascript/packages/document-capture/components/selfie-step-spec.tsx
@@ -0,0 +1,106 @@
+import userEvent from '@testing-library/user-event';
+import sinon from 'sinon';
+import { expect } from 'chai';
+import { FailedCaptureAttemptsContextProvider } from '@18f/identity-document-capture';
+import SelfieCaptureContext from '@18f/identity-document-capture/context/selfie-capture';
+import SelfieStep from '@18f/identity-document-capture/components/selfie-step';
+import { render } from '../../../support/document-capture';
+import { getFixtureFile } from '../../../support/file';
+
+describe('document-capture/components/selfie-step', () => {
+ let getByLabelText;
+ let queryByLabelText;
+
+ context('when initially shown', () => {
+ beforeEach(() => {
+ ({ queryByLabelText } = render(
+ undefined}
+ errors={[]}
+ onError={() => undefined}
+ registerField={() => undefined}
+ unknownFieldErrors={[]}
+ toPreviousStep={() => undefined}
+ />,
+ ));
+ });
+ });
+
+ context('when show help is turned off ', () => {
+ beforeEach(() => {
+ ({ queryByLabelText } = render(
+
+ undefined}
+ errors={[]}
+ onError={() => undefined}
+ registerField={() => undefined}
+ unknownFieldErrors={[]}
+ toPreviousStep={() => undefined}
+ />
+ ,
+ ,
+ ));
+ });
+
+ it('renders with only selfie input', () => {
+ const front = queryByLabelText('doc_auth.headings.document_capture_front');
+ const back = queryByLabelText('doc_auth.headings.document_capture_back');
+ const selfie = queryByLabelText('doc_auth.headings.document_capture_selfie');
+
+ expect(front).to.not.exist();
+ expect(back).to.not.exist();
+ expect(selfie).to.be.ok();
+ });
+ });
+
+ it('calls onChange callback with uploaded image', async () => {
+ const onChange = sinon.stub();
+ ({ getByLabelText } = render(
+
+
+ undefined}
+ registerField={() => undefined}
+ unknownFieldErrors={[]}
+ toPreviousStep={() => undefined}
+ />
+
+ ,
+ ,
+ ));
+ const file = await getFixtureFile('doc_auth_images/id-back.jpg');
+
+ await Promise.all([
+ new Promise((resolve) => onChange.callsFake(resolve)),
+ userEvent.upload(getByLabelText('doc_auth.headings.document_capture_selfie'), file),
+ ]);
+ expect(onChange).to.have.been.calledWith({
+ selfie: file,
+ selfie_image_metadata: sinon.match(/^\{.+\}$/),
+ });
+ });
+});
diff --git a/spec/javascript/packages/document-capture/context/selfie-capture-spec.jsx b/spec/javascript/packages/document-capture/context/selfie-capture-spec.jsx
index a02a9e3a7e6..796377d6da3 100644
--- a/spec/javascript/packages/document-capture/context/selfie-capture-spec.jsx
+++ b/spec/javascript/packages/document-capture/context/selfie-capture-spec.jsx
@@ -9,7 +9,8 @@ describe('document-capture/context/selfie-capture', () => {
expect(result.current).to.have.keys([
'isSelfieCaptureEnabled',
'isSelfieDesktopTestMode',
- 'docAuthSeparatePagesEnabled',
+ 'showHelpInitially',
+ 'immediatelyBeginCapture',
]);
expect(result.current.isSelfieCaptureEnabled).to.be.a('boolean');
});
diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb
index bb151c2a080..aa7a524833e 100644
--- a/spec/jobs/get_usps_proofing_results_job_spec.rb
+++ b/spec/jobs/get_usps_proofing_results_job_spec.rb
@@ -103,6 +103,8 @@
let(:send_proofing_notification_job) do
double(InPerson::SendProofingNotificationJob)
end
+ let(:no_visited_location_name) { 'none' }
+ let(:visited_location_name) { 'WILKES BARRE' }
let(:enrollment) do
create(:in_person_enrollment, :pending, :with_notification_phone_configuration)
end
@@ -324,6 +326,7 @@
it 'sends an in person deadline passed email' do
expect(user_mailer).to have_received(:in_person_deadline_passed).with(
enrollment: enrollment,
+ visited_location_name: no_visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(no_args)
end
@@ -496,6 +499,7 @@
it 'sends an in person deadline passed email' do
expect(user_mailer).to have_received(:in_person_deadline_passed).with(
enrollment: enrollment,
+ visited_location_name: no_visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(no_args)
end
@@ -694,6 +698,7 @@
it 'sends an in person deadline passed email' do
expect(user_mailer).to have_received(:in_person_deadline_passed).with(
enrollment: enrollment,
+ visited_location_name: no_visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(no_args)
end
@@ -1539,6 +1544,7 @@
it 'sends the please call email' do
expect(user_mailer).to have_received(:in_person_please_call).with(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(
queue: :intentionally_delayed,
@@ -1650,6 +1656,7 @@
it 'sends the in person failed email' do
expect(user_mailer).to have_received(:in_person_failed).with(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(
queue: :intentionally_delayed,
@@ -1753,6 +1760,7 @@
it 'sends the in person failed email with delay' do
expect(user_mailer).to have_received(:in_person_failed).with(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(
queue: :intentionally_delayed,
@@ -1851,6 +1859,7 @@
it 'sends the in person verified email with a delay' do
expect(user_mailer).to have_received(:in_person_verified).with(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(
queue: :intentionally_delayed,
@@ -1950,6 +1959,7 @@
it 'sends the in person failed email with delay' do
expect(user_mailer).to have_received(:in_person_failed).with(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(
queue: :intentionally_delayed,
@@ -2070,6 +2080,7 @@
it 'sends the in person verified email with delay' do
expect(user_mailer).to have_received(:in_person_verified).with(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(
queue: :intentionally_delayed,
@@ -2168,6 +2179,7 @@
it 'sends the in person verified email' do
expect(user_mailer).to have_received(:in_person_verified).with(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(
queue: :intentionally_delayed,
@@ -2267,6 +2279,7 @@
it 'sends the in person verified email' do
expect(user_mailer).to have_received(:in_person_verified).with(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(
queue: :intentionally_delayed,
@@ -2371,6 +2384,7 @@
it 'sends the in person failed email' do
expect(user_mailer).to have_received(:in_person_failed).with(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(
queue: :intentionally_delayed,
@@ -2474,6 +2488,7 @@
it 'sends the in person failed fraud email with a delay' do
expect(user_mailer).to have_received(:in_person_failed_fraud).with(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(
queue: :intentionally_delayed,
@@ -2610,6 +2625,7 @@
it 'sends the in person verified email without delay' do
expect(user_mailer).to have_received(:in_person_verified).with(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(no_args)
end
@@ -2638,6 +2654,7 @@
it 'sends the in person verified email with a default 1 hour delay' do
expect(user_mailer).to have_received(:in_person_verified).with(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
expect(mail_deliverer).to have_received(:deliver_later).with(
queue: :intentionally_delayed,
diff --git a/spec/jobs/good_job_v4_ready_job_spec.rb b/spec/jobs/good_job_v4_ready_job_spec.rb
new file mode 100644
index 00000000000..0a5c60645bb
--- /dev/null
+++ b/spec/jobs/good_job_v4_ready_job_spec.rb
@@ -0,0 +1,19 @@
+require 'rails_helper'
+
+RSpec.describe GoodJobV4ReadyJob, type: :job do
+ describe '#perform' do
+ it 'logs goodjob v4 readiness' do
+ expect(Rails.logger).to receive(:info) do |str|
+ msg = JSON.parse(str, symbolize_names: true)
+ expect(msg).to eq(
+ {
+ name: 'good_job_v4_ready',
+ ready: GoodJob.v4_ready?,
+ },
+ )
+ end
+
+ GoodJobV4ReadyJob.new.perform
+ end
+ end
+end
diff --git a/spec/jobs/in_person/send_proofing_notification_job_spec.rb b/spec/jobs/in_person/send_proofing_notification_job_spec.rb
index af1eac95345..efdc8ef29ed 100644
--- a/spec/jobs/in_person/send_proofing_notification_job_spec.rb
+++ b/spec/jobs/in_person/send_proofing_notification_job_spec.rb
@@ -88,7 +88,7 @@
context 'enrollment does not exist' do
it 'returns without doing anything' do
- bad_id = (InPersonEnrollment.all.pluck(:id).max || 0) + 1
+ bad_id = (InPersonEnrollment.pluck(:id).max || 0) + 1
job.perform(bad_id)
expect(analytics).not_to have_logged_event('SendProofingNotificationJob: job started')
expect(analytics).to have_logged_event('SendProofingNotificationJob: job skipped')
diff --git a/spec/jobs/socure_reason_code_download_job_spec.rb b/spec/jobs/socure_reason_code_download_job_spec.rb
new file mode 100644
index 00000000000..61beb823f80
--- /dev/null
+++ b/spec/jobs/socure_reason_code_download_job_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe SocureReasonCodeDownloadJob do
+ subject(:job) { described_class.new }
+
+ let(:idv_socure_reason_code_download_enabled) { true }
+ let(:analytics) { FakeAnalytics.new }
+
+ let(:api_response_body) do
+ {
+ 'reasonCodes' => {
+ 'ProductA' => {
+ 'A1' => 'test1',
+ 'A2' => 'test2',
+ },
+ 'ProductB' => {
+ 'B2' => 'test3',
+ },
+ },
+ }.to_json
+ end
+
+ before do
+ allow(IdentityConfig.store).to receive(:idv_socure_reason_code_download_enabled).
+ and_return(idv_socure_reason_code_download_enabled)
+ allow(IdentityConfig.store).to receive(:socure_reason_code_base_url).
+ and_return('https://example.org')
+ end
+
+ describe '#perform' do
+ it 'downloads reason codes and writes them to the database' do
+ stub_request(:get, 'https://example.org/api/3.0/reasoncodes?group=true').to_return(
+ headers: { 'Content-Type' => 'application/json' },
+ body: api_response_body,
+ )
+
+ expect { job.perform }.to change { SocureReasonCode.count }.from(0).to(3)
+ end
+
+ it 'logs an analytics event' do
+ allow(job).to receive(:analytics).and_return(analytics)
+ stub_request(:get, 'https://example.org/api/3.0/reasoncodes?group=true').to_return(
+ headers: { 'Content-Type' => 'application/json' },
+ body: api_response_body,
+ )
+
+ job.perform
+
+ expect(analytics).to have_logged_event(
+ :idv_socure_reason_code_download,
+ success: true,
+ added_reason_codes: [
+ { 'code' => 'A1', 'group' => 'ProductA', 'description' => 'test1' },
+ { 'code' => 'A2', 'group' => 'ProductA', 'description' => 'test2' },
+ { 'code' => 'B2', 'group' => 'ProductB', 'description' => 'test3' },
+ ],
+ deactivated_reason_codes: [],
+ )
+ end
+
+ context 'when an error occurs downloading the codes' do
+ it 'logs the error' do
+ allow(job).to receive(:analytics).and_return(analytics)
+ stub_request(:get, 'https://example.org/api/3.0/reasoncodes?group=true').to_timeout
+
+ expect { job.perform }.to_not change { SocureReasonCode.count }
+
+ expect(analytics).to have_logged_event(
+ :idv_socure_reason_code_download,
+ success: false,
+ exception: a_string_matching(/execution expired/),
+ )
+ end
+ end
+
+ context 'when the job is disabled' do
+ let(:idv_socure_reason_code_download_enabled) { false }
+
+ it 'does not download codes and does not write anything to the database' do
+ allow(job).to receive(:analytics).and_return(analytics)
+ api_response_body = { 'reasonCodes' => { 'A1' => 'test1', 'B2' => 'test2' } }.to_json
+ stub_request(:get, 'https://example.org/api/3.0/reasoncodes?group=true').to_return(
+ headers: { 'Content-Type' => 'application/json' },
+ body: api_response_body,
+ )
+
+ expect { job.perform }.to_not change { SocureReasonCode.count }
+ end
+ end
+ end
+end
diff --git a/spec/lib/identity_config_spec.rb b/spec/lib/identity_config_spec.rb
index 418a868b6ba..2610aaf78ca 100644
--- a/spec/lib/identity_config_spec.rb
+++ b/spec/lib/identity_config_spec.rb
@@ -8,6 +8,15 @@
describe '.key_types' do
subject(:key_types) { Identity::Hostdata.config_builder.key_types }
+ it 'has defaults defined for all keys in default configuration' do
+ aggregate_failures do
+ key_types.keys.each do |key|
+ expect(default_yaml_config).
+ to have_key(key.to_s), "expected default configuration to include value for #{key}"
+ end
+ end
+ end
+
it 'has all _enabled keys as booleans' do
aggregate_failures do
key_types.select { |key, _type| key.to_s.end_with?('_enabled') }.
diff --git a/spec/lib/identity_job_log_subscriber_spec.rb b/spec/lib/identity_job_log_subscriber_spec.rb
index 4a96e177545..7162f1cbf0d 100644
--- a/spec/lib/identity_job_log_subscriber_spec.rb
+++ b/spec/lib/identity_job_log_subscriber_spec.rb
@@ -14,6 +14,8 @@
expect(json['job_class']).to eq('AddressProofingJob')
expect(json.key?('trace_id'))
expect(json.key?('duration_ms'))
+ expect(json.key?('cpu_time_ms'))
+ expect(json.key?('idle_time_ms'))
expect(json.key?('job_id'))
expect(json.key?('timestamp'))
end
@@ -65,6 +67,8 @@
'RetryEvent',
payload: { wait: 1, job: double('Job', job_id: '1', queue_name: 'Default', arguments: []) },
duration: 1,
+ cpu_time: 1,
+ idle_time: 1,
name: 'TestEvent',
)
@@ -85,6 +89,8 @@
error: double('Exception'),
},
duration: 1,
+ cpu_time: 1,
+ idle_time: 1,
name: 'TestEvent',
)
@@ -120,6 +126,8 @@
expect(payload).to match(
duration_ms: kind_of(Numeric),
+ cpu_time_ms: kind_of(Numeric),
+ idle_time_ms: kind_of(Numeric),
exception_class_warn: 'ActiveRecord::RecordNotUnique',
exception_message_warn: /(cron_key, cron_at)/,
job_class: 'HeartbeatJob',
@@ -156,6 +164,8 @@
expect(payload).to match(
duration_ms: kind_of(Float),
+ cpu_time_ms: kind_of(Numeric),
+ idle_time_ms: kind_of(Numeric),
exception_class_warn: 'Errno::ECONNREFUSED',
exception_message_warn: 'Connection refused',
job_class: 'RiscDeliveryJob',
@@ -193,6 +203,8 @@
expect(payload).to match(
duration_ms: kind_of(Float),
+ cpu_time_ms: kind_of(Numeric),
+ idle_time_ms: kind_of(Numeric),
halted: true,
job_class: 'RiscDeliveryJob',
job_id: job.job_id,
@@ -229,6 +241,8 @@
expect(payload).to match(
duration_ms: kind_of(Float),
+ cpu_time_ms: kind_of(Numeric),
+ idle_time_ms: kind_of(Numeric),
timestamp: kind_of(String),
name: 'enqueue.active_job',
job_class: 'RiscDeliveryJob',
@@ -292,6 +306,8 @@
expect(payload).to match(
duration_ms: kind_of(Float),
+ cpu_time_ms: kind_of(Numeric),
+ idle_time_ms: kind_of(Numeric),
exception_class_warn: 'Errno::ECONNREFUSED',
exception_message_warn: 'Connection refused',
job_class: 'RiscDeliveryJob',
@@ -350,6 +366,8 @@ def perform(_); end
expect(payload).to match(
duration_ms: kind_of(Float),
+ cpu_time_ms: kind_of(Numeric),
+ idle_time_ms: kind_of(Numeric),
halted: true,
job_class: 'RiscDeliveryJob',
job_id: job.job_id,
@@ -386,6 +404,8 @@ def perform(_); end
expect(payload).to match(
duration_ms: kind_of(Float),
+ cpu_time_ms: kind_of(Numeric),
+ idle_time_ms: kind_of(Numeric),
timestamp: kind_of(String),
name: 'enqueue.active_job',
job_class: 'RiscDeliveryJob',
@@ -428,6 +448,8 @@ def perform(_); end
expect(payload).to match(
duration_ms: kind_of(Float),
+ cpu_time_ms: kind_of(Numeric),
+ idle_time_ms: kind_of(Numeric),
timestamp: kind_of(String),
name: 'enqueue.active_job',
job_class: 'RiscDeliveryJob',
diff --git a/spec/mailers/previews/report_mailer_preview.rb b/spec/mailers/previews/report_mailer_preview.rb
index a3a831ebcf2..0529ee4646a 100644
--- a/spec/mailers/previews/report_mailer_preview.rb
+++ b/spec/mailers/previews/report_mailer_preview.rb
@@ -23,6 +23,21 @@ def monthly_key_metrics_report
)
end
+ def protocols_report
+ date = Time.zone.yesterday
+ report = Reports::ProtocolsReport.new(date)
+
+ stub_cloudwatch_client(report.send(:report))
+
+ ReportMailer.tables_report(
+ email: 'test@example.com',
+ subject: "Weekly Protocols Report - #{date}",
+ message: "Report: protocols-report #{date}",
+ attachment_format: :csv,
+ reports: report.send(:weekly_protocols_emailable_reports),
+ )
+ end
+
def fraud_metrics_report
fraud_metrics_report = Reports::FraudMetricsReport.new(Time.zone.yesterday)
diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb
index 05b63b0aaa8..ec81d914965 100644
--- a/spec/mailers/previews/user_mailer_preview.rb
+++ b/spec/mailers/previews/user_mailer_preview.rb
@@ -164,6 +164,7 @@ def in_person_completion_survey
def in_person_deadline_passed
UserMailer.with(user: user, email_address: email_address_record).in_person_deadline_passed(
enrollment: in_person_enrollment_id_ipp,
+ visited_location_name: in_person_visited_location_name,
)
end
@@ -202,24 +203,28 @@ def in_person_ready_to_verify_reminder_enhanced_ipp_enabled
def in_person_verified
UserMailer.with(user: user, email_address: email_address_record).in_person_verified(
enrollment: in_person_enrollment_id_ipp,
+ visited_location_name: in_person_visited_location_name,
)
end
def in_person_failed
UserMailer.with(user: user, email_address: email_address_record).in_person_failed(
enrollment: in_person_enrollment_id_ipp,
+ visited_location_name: in_person_visited_location_name,
)
end
def in_person_failed_fraud
UserMailer.with(user: user, email_address: email_address_record).in_person_failed_fraud(
enrollment: in_person_enrollment_id_ipp,
+ visited_location_name: in_person_visited_location_name,
)
end
def in_person_please_call
UserMailer.with(user: user, email_address: email_address_record).in_person_please_call(
enrollment: in_person_enrollment_id_ipp,
+ visited_location_name: in_person_visited_location_name,
)
end
@@ -301,6 +306,10 @@ def email_address_record
unsaveable(EmailAddress.new(email: email_address))
end
+ def in_person_visited_location_name
+ 'ACQUAINTANCESHIP'
+ end
+
def in_person_enrollment_id_ipp
unsaveable(
InPersonEnrollment.new(
diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb
index ff529f49665..87337726d98 100644
--- a/spec/mailers/user_mailer_spec.rb
+++ b/spec/mailers/user_mailer_spec.rb
@@ -565,6 +565,7 @@ def expect_email_body_to_have_help_and_contact_links
:enhanced_ipp,
)
end
+ let(:visited_location_name) { 'ACQUAINTANCESHIP' }
describe '#in_person_ready_to_verify' do
let(:mail) do
@@ -875,6 +876,7 @@ def expect_email_body_to_have_help_and_contact_links
let(:mail) do
UserMailer.with(user: user, email_address: email_address).in_person_verified(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
end
@@ -895,6 +897,7 @@ def expect_email_body_to_have_help_and_contact_links
let(:mail) do
UserMailer.with(user: user, email_address: email_address).in_person_failed(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
end
@@ -914,6 +917,7 @@ def expect_email_body_to_have_help_and_contact_links
let(:mail) do
UserMailer.with(user: user, email_address: email_address).in_person_failed_fraud(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
end
@@ -925,6 +929,7 @@ def expect_email_body_to_have_help_and_contact_links
let(:mail) do
UserMailer.with(user: user, email_address: email_address).in_person_please_call(
enrollment: enrollment,
+ visited_location_name: visited_location_name,
)
end
diff --git a/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb b/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb
index 4b53c3ae6da..e320151a0a7 100644
--- a/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb
+++ b/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb
@@ -3,7 +3,7 @@
RSpec.describe Idv::InPerson::VerificationResultsEmailPresenter do
include Rails.application.routes.url_helpers
- let(:location_name) { 'FRIENDSHIP' }
+ let(:visited_location_name) { 'ACQUAINTANCESHIP' }
let(:status_updated_at) { described_class::USPS_SERVER_TIMEZONE.parse('2022-07-14T00:00:00Z') }
let(:sp) { nil }
let(:current_address_matches_id) { true }
@@ -12,16 +12,21 @@
:in_person_enrollment,
:pending,
service_provider: sp,
- selected_location_details: { name: location_name },
+ selected_location_details: { name: 'FRIENDSHIP' },
current_address_matches_id: current_address_matches_id,
)
end
- subject(:presenter) { described_class.new(enrollment: enrollment, url_options: {}) }
+ subject(:presenter) do
+ described_class.new(
+ enrollment: enrollment, url_options: {},
+ visited_location_name: visited_location_name
+ )
+ end
- describe '#location_name' do
- it 'returns the enrollment location name' do
- expect(presenter.location_name).to eq(location_name)
+ describe 'visited_location_name' do
+ it 'returns the location that USPS reports the user visited for their proofing attempt' do
+ expect(presenter.visited_location_name).to eq(visited_location_name)
end
end
diff --git a/spec/services/agency_identity_linker_spec.rb b/spec/services/agency_identity_linker_spec.rb
index 32cde8fb0c2..991a90291ea 100644
--- a/spec/services/agency_identity_linker_spec.rb
+++ b/spec/services/agency_identity_linker_spec.rb
@@ -123,7 +123,7 @@
it 'persists the service provider identity as an agency identity' do
expect(subject.uuid).to eq uuid
- ai = AgencyIdentity.where(user: user, agency: agency).take
+ ai = AgencyIdentity.find_by(user: user, agency: agency)
expect(subject).to eq ai
end
end
diff --git a/spec/services/attribute_asserter_spec.rb b/spec/services/attribute_asserter_spec.rb
index 9132a9e9ae0..e74d4328d20 100644
--- a/spec/services/attribute_asserter_spec.rb
+++ b/spec/services/attribute_asserter_spec.rb
@@ -18,14 +18,13 @@
let(:name_id_format) { Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT }
let(:service_provider_ial) { 1 }
let(:service_provider_aal) { nil }
+ let(:attribute_bundle) { nil }
let(:service_provider) do
- instance_double(
- ServiceProvider,
- issuer: 'http://localhost:3000',
+ create(
+ :service_provider,
ial: service_provider_ial,
default_aal: service_provider_aal,
- metadata: {},
- semantic_authn_contexts_allowed?: false,
+ attribute_bundle:,
)
end
@@ -72,6 +71,11 @@
describe '#build' do
context 'when an IAL2 request is made' do
+ before do
+ user.identities << identity
+ subject.build
+ end
+
[
Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF,
Saml::Idp::Constants::IAL_VERIFIED_ACR,
@@ -85,12 +89,7 @@
context 'when the user has been proofed without facial match' do
context 'custom bundle includes email, phone, and first_name' do
- before do
- user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email phone first_name])
- subject.build
- end
+ let(:attribute_bundle) { %w[email phone first_name] }
it 'includes all requested attributes + uuid' do
expect(user.asserted_attributes.keys).
@@ -111,12 +110,7 @@
end
context 'custom bundle includes dob' do
- before do
- user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[dob])
- subject.build
- end
+ let(:attribute_bundle) { %w[dob] }
it 'formats the dob in an international format' do
expect(get_asserted_attribute(user, :dob)).to eq '1970-12-31'
@@ -124,12 +118,7 @@
end
context 'custom bundle includes zipcode' do
- before do
- user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[zipcode])
- subject.build
- end
+ let(:attribute_bundle) { %w[zipcode] }
it 'formats zipcode as 5 digits' do
expect(get_asserted_attribute(user, :zipcode)).to eq '12345'
@@ -137,12 +126,7 @@
end
context 'bundle includes :ascii' do
- before do
- user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email phone first_name ascii])
- subject.build
- end
+ let(:attribute_bundle) { %w[email phone first_name ascii] }
it 'skips ascii as an attribute' do
expect(user.asserted_attributes.keys).
@@ -155,12 +139,6 @@
end
context 'Service Provider does not specify bundle' do
- before do
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(nil)
- subject.build
- end
-
context 'authn request does not specify bundle' do
it 'only returns uuid, verified_at, aal, and ial' do
expect(user.asserted_attributes.keys).to eq %i[uuid verified_at aal ial]
@@ -186,11 +164,7 @@
end
context 'Service Provider specifies empty bundle' do
- before do
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return([])
- subject.build
- end
+ let(:attribute_bundle) { [] }
it 'only includes uuid, verified_at, aal, and ial' do
expect(user.asserted_attributes.keys).to eq(%i[uuid verified_at aal ial])
@@ -198,12 +172,7 @@
end
context 'custom bundle has invalid attribute name' do
- before do
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).and_return(
- %w[email foo],
- )
- subject.build
- end
+ let(:attribute_bundle) { %w[email foo] }
it 'silently skips invalid attribute name' do
expect(user.asserted_attributes.keys).to eq(%i[uuid email verified_at aal ial])
@@ -211,10 +180,8 @@
end
context 'x509 attributes included in the SP attribute bundle' do
- before do
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email x509_subject x509_issuer x509_presented])
- subject.build
+ let(:attribute_bundle) do
+ %w[email x509_subject x509_issuer x509_presented]
end
context 'user did not present piv/cac' do
@@ -251,11 +218,6 @@
context 'when the user has been proofed with facial match' do
let(:user) { create(:profile, :active, :verified, idv_level: :in_person).user }
- before do
- user.identities << identity
- subject.build
- end
-
it 'asserts IAL2' do
expected_ial = Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF
expect(get_asserted_attribute(user, :ial)).to eq expected_ial
@@ -266,11 +228,9 @@
context 'verified user and proofing VTR request' do
let(:authn_context) { 'C1.C2.P1' }
-
+ let(:attribute_bundle) { %w[email first_name last_name] }
before do
user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email first_name last_name])
subject.build
end
@@ -284,14 +244,14 @@
end
context 'when an IAL1 request is made' do
+ before do
+ user.identities << identity
+ subject.build
+ end
+
context 'when the user has been proofed without facial match comparison' do
context 'custom bundle includes email, phone, and first_name' do
- before do
- user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email phone first_name])
- subject.build
- end
+ let(:attribute_bundle) { %w[email phone first_name] }
it 'only includes uuid, email, aal, and ial (no verified_at)' do
expect(user.asserted_attributes.keys).to eq %i[uuid email aal ial]
@@ -309,12 +269,7 @@
end
context 'custom bundle includes verified_at' do
- before do
- user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email verified_at])
- subject.build
- end
+ let(:attribute_bundle) { %w[email verified_at] }
context 'the service provider is ial1' do
let(:service_provider_ial) { 1 }
@@ -343,12 +298,6 @@
end
context 'Service Provider does not specify bundle' do
- before do
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(nil)
- subject.build
- end
-
context 'authn request does not specify bundle' do
it 'only includes uuid, aal, and ial' do
expect(user.asserted_attributes.keys).to eq %i[uuid aal ial]
@@ -374,11 +323,7 @@
end
context 'Service Provider specifies empty bundle' do
- before do
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return([])
- subject.build
- end
+ let(:attribute_bundle) { [] }
it 'only includes UUID, aal, and ial' do
expect(user.asserted_attributes.keys).to eq(%i[uuid aal ial])
@@ -386,12 +331,7 @@
end
context 'custom bundle has invalid attribute name' do
- before do
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).and_return(
- %w[email foo],
- )
- subject.build
- end
+ let(:attribute_bundle) { %w[email foo] }
it 'silently skips invalid attribute name' do
expect(user.asserted_attributes.keys).to eq(%i[uuid email aal ial])
@@ -399,11 +339,7 @@
end
context 'x509 attributes included in the SP attribute bundle' do
- before do
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email x509_subject x509_issuer x509_presented])
- subject.build
- end
+ let(:attribute_bundle) { %w[email x509_subject x509_issuer x509_presented] }
context 'user did not present piv/cac' do
let(:user_session) do
@@ -436,13 +372,7 @@
context 'request made with a VTR param' do
let(:options) { { authn_context: 'C1.C2' } }
-
- before do
- user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email])
- subject.build
- end
+ let(:attribute_bundle) { %w[email] }
it 'includes the correct bundle attributes' do
expect(user.asserted_attributes.keys).to eq(
@@ -456,11 +386,6 @@
context 'when the user has been proofed with facial match comparison' do
let(:user) { create(:profile, :active, :verified, idv_level: :in_person).user }
- before do
- user.identities << identity
- subject.build
- end
-
it 'asserts IAL1' do
expected_ial = Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF
expect(get_asserted_attribute(user, :ial)).to eq expected_ial
@@ -469,6 +394,13 @@
end
context 'verified user and IAL1 AAL3 request' do
+ let(:attribute_bundle) { %w[email phone first_name] }
+
+ before do
+ user.identities << identity
+ subject.build
+ end
+
context 'service provider configured for AAL3' do
let(:service_provider_aal) { 3 }
let(:authn_context) do
@@ -478,13 +410,6 @@
]
end
- before do
- user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email phone first_name])
- subject.build
- end
-
it 'asserts AAL3' do
expected_aal = Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF
expect(get_asserted_attribute(user, :aal)).to eq expected_aal
@@ -499,13 +424,6 @@
]
end
- before do
- user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email phone first_name])
- subject.build
- end
-
it 'asserts AAL3' do
expected_aal = Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF
expect(get_asserted_attribute(user, :aal)).to eq expected_aal
@@ -543,6 +461,7 @@
context 'IAL2 service provider requests IALMAX with IAL2 user' do
let(:service_provider_ial) { 2 }
+ let(:attribute_bundle) { %w[email phone first_name] }
let(:options) do
{
authn_context: [
@@ -554,8 +473,6 @@
before do
user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email phone first_name])
ServiceProvider.find_by(issuer: sp1_issuer).update!(ial: 2)
subject.build
end
@@ -574,6 +491,7 @@
context 'non-IAL2 service provider requests IALMAX with IAL2 user' do
let(:service_provider_ial) { 1 }
+ let(:attribute_bundle) { %w[email phone first_name] }
let(:options) do
{
authn_context: [
@@ -585,8 +503,6 @@
before do
user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email phone first_name])
ServiceProvider.find_by(issuer: sp1_issuer).update!(ial: 1)
subject.build
end
@@ -611,14 +527,14 @@
}
end
+ before do
+ user.identities << identity
+ subject.build
+ end
+
context 'when the user has been proofed with facial match' do
let(:user) { facial_match_verified_user }
- before do
- user.identities << identity
- subject.build
- end
-
it 'asserts IAL2 with facial match comparison' do
expected_ial = Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF
expect(get_asserted_attribute(user, :ial)).to eq expected_ial
@@ -626,11 +542,6 @@
end
context 'when the user has been proofed without facial match' do
- before do
- user.identities << identity
- subject.build
- end
-
it 'asserts IAL2 (without facial match comparison)' do
expected_ial = Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF
expect(get_asserted_attribute(user, :ial)).to eq expected_ial
@@ -664,12 +575,10 @@
shared_examples 'unverified user' do
let(:user) { create(:user, :fully_registered) }
+ let(:attribute_bundle) { %w[first_name last_name] }
context 'custom bundle does not include email, phone' do
before do
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).and_return(
- %w[first_name last_name],
- )
subject.build
end
@@ -679,11 +588,9 @@
end
context 'custom bundle includes all_emails' do
+ let(:attribute_bundle) { %w[all_emails] }
before do
create(:email_address, user: user)
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).and_return(
- %w[all_emails],
- )
subject.build
end
@@ -696,10 +603,8 @@
end
context 'custom bundle includes email, phone' do
+ let(:attribute_bundle) { %w[first_name last_name email phone] }
before do
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).and_return(
- %w[first_name last_name email phone],
- )
subject.build
end
@@ -733,6 +638,7 @@
end
context 'with a deleted email' do
+ let(:attribute_bundle) { %w[email phone first_name] }
let(:subject) do
described_class.new(
user: user,
@@ -746,8 +652,6 @@
before do
user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email phone first_name])
create(:email_address, user:, email: 'email@example.com')
ident = user.identities.last
@@ -767,6 +671,7 @@
end
context 'with a nil email id' do
+ let(:attribute_bundle) { %w[email phone first_name] }
let(:subject) do
described_class.new(
user: user,
@@ -780,8 +685,6 @@
before do
user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email phone first_name])
ident = user.identities.last
ident.email_address_id = nil
@@ -796,6 +699,7 @@
end
context 'select email to send to partner feature is disabled' do
+ let(:attribute_bundle) { %w[first_name last_name email phone] }
before do
allow(IdentityConfig.store).to receive(
:feature_select_email_to_share_enabled,
@@ -816,8 +720,6 @@
before do
user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email phone first_name])
create(:email_address, user:, email: 'email@example.com')
ident = user.identities.last
@@ -837,6 +739,7 @@
end
context 'with a nil email id' do
+ let(:attribute_bundle) { %w[first_name email phone] }
let(:subject) do
described_class.new(
user: user,
@@ -850,8 +753,6 @@
before do
user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email phone first_name])
ident = user.identities.last
ident.email_address_id = nil
@@ -868,10 +769,9 @@
end
describe 'aal attributes handling' do
+ let(:attribute_bundle) { %w[email] }
before do
user.identities << identity
- allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle).
- and_return(%w[email])
subject.build
end
diff --git a/spec/services/funnel/registration/add_mfa_spec.rb b/spec/services/funnel/registration/add_mfa_spec.rb
index 6fde1986586..58d977ff077 100644
--- a/spec/services/funnel/registration/add_mfa_spec.rb
+++ b/spec/services/funnel/registration/add_mfa_spec.rb
@@ -8,7 +8,7 @@
user = create(:user)
user.id
end
- let(:funnel) { RegistrationLog.all.first }
+ let(:funnel) { RegistrationLog.first }
it 'shows user is not fully registered with no mfa' do
expect(funnel&.registered_at).to_not be_present
diff --git a/spec/services/proofing/socure/reason_codes/api_client_spec.rb b/spec/services/proofing/socure/reason_codes/api_client_spec.rb
new file mode 100644
index 00000000000..0d6fafa9f2f
--- /dev/null
+++ b/spec/services/proofing/socure/reason_codes/api_client_spec.rb
@@ -0,0 +1,87 @@
+require 'rails_helper'
+
+RSpec.describe Proofing::Socure::ReasonCodes::ApiClient do
+ before do
+ allow(IdentityConfig.store).to receive(
+ :socure_reason_code_base_url,
+ ).and_return(
+ 'https://example.org/',
+ )
+ end
+
+ it 'returns a parsed set or reason codes' do
+ api_response_body = {
+ 'reasonCodes' => {
+ 'ProductA' => {
+ 'A1' => 'test1',
+ 'A2' => 'test2',
+ },
+ 'ProductB' => {
+ 'B2' => 'test3',
+ },
+ },
+ }.to_json
+ stub_request(:get, 'https://example.org/api/3.0/reasoncodes?group=true').to_return(
+ headers: { 'Content-Type' => 'application/json' },
+ body: api_response_body,
+ )
+
+ result = described_class.new.download_reason_codes
+
+ expect(result).to eq(
+ 'ProductA' => {
+ 'A1' => 'test1',
+ 'A2' => 'test2',
+ },
+ 'ProductB' => {
+ 'B2' => 'test3',
+ },
+ )
+ end
+
+ context 'the authentication to the service fails' do
+ it 'raises an unauthorized error' do
+ stub_request(:get, 'https://example.org/api/3.0/reasoncodes?group=true').to_return(
+ status: 401,
+ headers: {
+ 'Content-Type' => 'application/json',
+ },
+ body: {
+ status: 'Error',
+ referenceId: 'a-big-unique-reference-id',
+ msg: 'Request-specific error message goes here',
+ }.to_json,
+ )
+
+ expect { described_class.new.download_reason_codes }.to raise_error(
+ Proofing::Socure::ReasonCodes::ApiClient::ApiClientError,
+ 'the server responded with status 401',
+ )
+ end
+ end
+
+ context 'there is a networking error in the request' do
+ it 'raises the error' do
+ stub_request(:get, 'https://example.org/api/3.0/reasoncodes?group=true').to_timeout
+
+ expect { described_class.new.download_reason_codes }.to raise_error(
+ Proofing::Socure::ReasonCodes::ApiClient::ApiClientError,
+ 'execution expired',
+ )
+ end
+ end
+
+ context 'the response includes invalid JSON' do
+ it 'raises a parsing error' do
+ stub_request(:get, 'https://example.org/api/3.0/reasoncodes?group=true').to_return(
+ headers: { 'Content-Type' => 'application/json' },
+ body: '{;*[("',
+ )
+
+ expect { described_class.new.download_reason_codes }.to raise_error(
+ Proofing::Socure::ReasonCodes::ApiClient::ApiClientError,
+ "unexpected token at '{;*[(\"'",
+ )
+ end
+ end
+end
diff --git a/spec/services/proofing/socure/reason_codes/importer_spec.rb b/spec/services/proofing/socure/reason_codes/importer_spec.rb
new file mode 100644
index 00000000000..f14ef0ac5af
--- /dev/null
+++ b/spec/services/proofing/socure/reason_codes/importer_spec.rb
@@ -0,0 +1,91 @@
+require 'rails_helper'
+
+RSpec.describe Proofing::Socure::ReasonCodes::Importer do
+ describe '#download' do
+ let(:downloaded_reason_codes) do
+ {
+ 'ProductA' => {
+ 'A1' => 'test1',
+ 'A2' => 'test2',
+ },
+ 'ProductB' => {
+ 'B2' => 'test3',
+ },
+ }
+ end
+
+ it 'adds reason codes that do not exist', :freeze_time do
+ allow(subject.api_client).to receive(:download_reason_codes).
+ and_return(downloaded_reason_codes)
+
+ result = subject.synchronize
+
+ expect(result.success?).to eq(true)
+ expect(result.to_h[:added_reason_codes]).to include(
+ 'code' => 'A1',
+ 'group' => 'ProductA',
+ 'description' => 'test1',
+ )
+
+ new_reason_code = SocureReasonCode.find_by(code: 'A1')
+ expect(new_reason_code.group).to eq('ProductA')
+ expect(new_reason_code.description).to eq('test1')
+ expect(new_reason_code.added_at).to be_within(1.second).of(Time.zone.now)
+ expect(new_reason_code.deactivated_at).to be_nil
+ end
+
+ it 'deactivates reason codes that have been removed by Socure', :freeze_time do
+ SocureReasonCode.create(
+ code: 'C3',
+ group: 'ProductC',
+ description: 'test3',
+ added_at: 1.day.ago,
+ )
+
+ allow(subject.api_client).to receive(:download_reason_codes).
+ and_return(downloaded_reason_codes)
+
+ result = subject.synchronize
+ expect(result.to_h[:deactivated_reason_codes]).to eq(
+ [{ 'code' => 'C3', 'group' => 'ProductC', 'description' => 'test3' }],
+ )
+
+ expect(result.success?).to eq(true)
+
+ deactivated_reason_code = SocureReasonCode.find_by(code: 'C3')
+ expect(deactivated_reason_code.deactivated_at).to be_within(1.second).of(Time.zone.now)
+ end
+
+ context 'the downloaded reason codes are malformed' do
+ it 'returns an unsuccessful response' do
+ allow(subject.api_client).to receive(:download_reason_codes).
+ and_return('malformed response')
+
+ result = subject.synchronize
+
+ expect(result.success?).to eq(false)
+ expect(result.to_h[:exception]).to include(
+ 'Expected "malformed response" to be a hash of reason codes',
+ )
+ end
+ end
+
+ context 'their is a networking error downloading codes' do
+ it 'returns an unsuccessful response' do
+ allow(subject.api_client).to receive(
+ :download_reason_codes,
+ ).and_raise(
+ Proofing::Socure::ReasonCodes::ApiClient::ApiClientError,
+ 'test error',
+ )
+
+ result = subject.synchronize
+
+ expect(result.success?).to eq(false)
+ expect(result.to_h[:exception]).to eq(
+ '#',
+ )
+ end
+ end
+ end
+end
diff --git a/spec/support/ab_tests_helper.rb b/spec/support/ab_tests_helper.rb
index 8f8f4742a14..f37e504baa3 100644
--- a/spec/support/ab_tests_helper.rb
+++ b/spec/support/ab_tests_helper.rb
@@ -1,8 +1,10 @@
module AbTestsHelper
def reload_ab_tests
+ # rubocop:disable Rails/FindEach
AbTests.all.each do |(name, _)|
AbTests.send(:remove_const, name)
end
+ # rubocop:enable Rails/FindEach
load('config/initializers/ab_tests.rb')
end
end
diff --git a/spec/support/features/doc_capture_helper.rb b/spec/support/features/doc_capture_helper.rb
index 48b0372f4ca..3b796c4f7ff 100644
--- a/spec/support/features/doc_capture_helper.rb
+++ b/spec/support/features/doc_capture_helper.rb
@@ -51,10 +51,6 @@ def expect_doc_capture_page_header(text)
expect(page).to have_css('.page-heading', text: text, wait: 5)
end
- def expect_doc_capture_id_subheader
- expect(page).to have_text(t('doc_auth.headings.document_capture_subheader_id'))
- end
-
def expect_doc_capture_selfie_subheader
expect(page).to have_text(t('doc_auth.headings.document_capture_subheader_selfie'))
end
diff --git a/spec/support/features/document_capture_step_helper.rb b/spec/support/features/document_capture_step_helper.rb
index 7a69f19be41..179b333acfb 100644
--- a/spec/support/features/document_capture_step_helper.rb
+++ b/spec/support/features/document_capture_step_helper.rb
@@ -25,9 +25,8 @@ def attach_liveness_images(
)
)
attach_images(file)
- if IdentityConfig.store.doc_auth_separate_pages_enabled
- click_continue
- end
+ click_continue
+ click_button 'Take photo' if page.has_button? 'Take photo'
attach_selfie
end
diff --git a/spec/support/features/idv_step_helper.rb b/spec/support/features/idv_step_helper.rb
index 51266c3b647..5833ef4d25d 100644
--- a/spec/support/features/idv_step_helper.rb
+++ b/spec/support/features/idv_step_helper.rb
@@ -77,14 +77,10 @@ def link_text
end
def click_sp_link_in_person_ready_to_verify
- expect(page).to have_content(sp_text)
+ expect(page).to have_content(link_text)
click_link(link_text)
end
- def sp_text
- t('in_person_proofing.body.barcode.return_to_partner_html', link_html: link_text)
- end
-
def complete_enter_password_step(user = user_with_2fa)
password = user.password || user_password
fill_in 'Password', with: password
diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb
index ea92023dd69..60ecb049ee1 100644
--- a/spec/support/features/session_helper.rb
+++ b/spec/support/features/session_helper.rb
@@ -292,8 +292,7 @@ def perform_in_browser(name)
end
def acknowledge_and_confirm_personal_key
- checkbox_header = t('forms.personal_key.required_checkbox')
- find('label', text: /#{checkbox_header}/).click
+ check t('forms.personal_key.required_checkbox')
click_continue
end
diff --git a/spec/support/idv_examples/sp_handoff.rb b/spec/support/idv_examples/sp_handoff.rb
index b071838fc51..282910ce4bd 100644
--- a/spec/support/idv_examples/sp_handoff.rb
+++ b/spec/support/idv_examples/sp_handoff.rb
@@ -1,5 +1,6 @@
RSpec.shared_examples 'sp handoff after identity verification' do |sp|
include SamlAuthHelper
+ include OidcAuthHelper
include IdvHelper
include JavascriptDriverHelper
@@ -134,43 +135,10 @@ def expect_csp_headers_to_be_present
end
def expect_successful_oidc_handoff
- redirect_uri = URI(current_url)
- redirect_params = Rack::Utils.parse_query(redirect_uri.query).with_indifferent_access
-
- expect(redirect_uri.to_s).to start_with('http://localhost:7654/auth/result')
- expect(redirect_params[:state]).to eq(@state)
-
- code = redirect_params[:code]
- expect(code).to be_present
-
- jwt_payload = {
- iss: @client_id,
- sub: @client_id,
- aud: api_openid_connect_token_url,
- jti: SecureRandom.hex,
- exp: 5.minutes.from_now.to_i,
- }
-
- client_assertion = JWT.encode(jwt_payload, client_private_key, 'RS256')
- client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
+ token_response = oidc_decoded_token
+ decoded_id_token = oidc_decoded_id_token
Capybara.using_driver(:desktop_rack_test) do
- page.driver.post api_openid_connect_token_path,
- grant_type: 'authorization_code',
- code: code,
- client_assertion_type: client_assertion_type,
- client_assertion: client_assertion
-
- expect(page.status_code).to eq(200)
- token_response = JSON.parse(page.body).with_indifferent_access
-
- id_token = token_response[:id_token]
- expect(id_token).to be_present
-
- decoded_id_token, _headers = JWT.decode(
- id_token, sp_public_key, true, algorithm: 'RS256'
- ).map(&:with_indifferent_access)
-
sub = decoded_id_token[:sub]
expect(sub).to be_present
expect(decoded_id_token[:nonce]).to eq(@nonce)
@@ -209,21 +177,4 @@ def expect_successful_saml_handoff
end
expect(xmldoc.phone_number.children.children.to_s).to eq(Phonelib.parse(profile_phone).e164)
end
-
- def client_private_key
- @client_private_key ||= begin
- OpenSSL::PKey::RSA.new(
- File.read(Rails.root.join('keys', 'saml_test_sp.key')),
- )
- end
- end
-
- def sp_public_key
- page.driver.get api_openid_connect_certs_path
-
- expect(page.status_code).to eq(200)
- certs_response = JSON.parse(page.body).with_indifferent_access
-
- JWT::JWK.import(certs_response[:keys].first).public_key
- end
end
diff --git a/spec/support/matchers/accessibility.rb b/spec/support/matchers/accessibility.rb
index 0f0de51e490..5221025ff52 100644
--- a/spec/support/matchers/accessibility.rb
+++ b/spec/support/matchers/accessibility.rb
@@ -3,6 +3,7 @@
match do |page|
['aria-describedby', 'aria-labelledby'].each do |idref_attribute|
+ # rubocop:disable Rails/FindEach
page.all(:css, "[#{idref_attribute}]").each do |element|
element[idref_attribute].split(' ').each do |referenced_id|
page.find_by_id(referenced_id, visible: :all)
@@ -10,6 +11,7 @@
rescue Capybara::ElementNotFound
invalid_idref_messages << "[#{idref_attribute}=\"#{element[idref_attribute]}\"]"
end
+ # rubocop:enable Rails/FindEach
end
invalid_idref_messages.blank?
@@ -28,9 +30,11 @@
elements = []
match do |page|
+ # rubocop:disable Rails/FindEach
page.all(:css, 'input[required]').each do |input|
elements << input if input['aria-invalid'].blank?
end
+ # rubocop:enable Rails/FindEach
elements.empty?
end
diff --git a/spec/support/oidc_auth_helper.rb b/spec/support/oidc_auth_helper.rb
index 58bac76d474..acfcfb75f79 100644
--- a/spec/support/oidc_auth_helper.rb
+++ b/spec/support/oidc_auth_helper.rb
@@ -1,4 +1,8 @@
+require_relative 'features/javascript_driver_helper'
+
module OidcAuthHelper
+ include JavascriptDriverHelper
+
OIDC_ISSUER = 'urn:gov:gsa:openidconnect:sp:server'.freeze
OIDC_IAL1_ISSUER = 'urn:gov:gsa:openidconnect:sp:server_ial1'.freeze
OIDC_AAL3_ISSUER = 'urn:gov:gsa:openidconnect:sp:server_requiring_aal3'.freeze
@@ -155,6 +159,9 @@ def extract_redirect_url
end
def oidc_redirect_url
+ # Page will redirect automatically if JavaScript is enabled
+ return current_url if javascript_enabled?
+
case IdentityConfig.store.openid_connect_redirect
when 'client_side'
extract_meta_refresh_url
@@ -164,4 +171,45 @@ def oidc_redirect_url
current_url
end
end
+
+ def oidc_decoded_token
+ return @oidc_decoded_token if defined?(@oidc_decoded_token)
+ redirect_uri = URI(oidc_redirect_url)
+ redirect_params = Rack::Utils.parse_query(redirect_uri.query).with_indifferent_access
+ code = redirect_params[:code]
+
+ jwt_payload = {
+ iss: 'urn:gov:gsa:openidconnect:sp:server',
+ sub: 'urn:gov:gsa:openidconnect:sp:server',
+ aud: api_openid_connect_token_url,
+ jti: SecureRandom.hex,
+ exp: 5.minutes.from_now.to_i,
+ }
+
+ client_private_key = OpenSSL::PKey::RSA.new(
+ File.read(Rails.root.join('keys', 'saml_test_sp.key')),
+ )
+ client_assertion = JWT.encode(jwt_payload, client_private_key, 'RS256')
+ client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
+
+ Capybara.using_driver(:desktop_rack_test) do
+ page.driver.post(
+ api_openid_connect_token_url,
+ grant_type: 'authorization_code',
+ code:,
+ client_assertion_type:,
+ client_assertion:,
+ )
+ @oidc_decoded_token = JSON.parse(page.body).with_indifferent_access
+ end
+ end
+
+ def oidc_decoded_id_token
+ @oidc_decoded_id_token ||= JWT.decode(
+ oidc_decoded_token[:id_token],
+ AppArtifacts.store.oidc_public_key,
+ true,
+ algorithm: 'RS256',
+ ).first.with_indifferent_access
+ end
end
diff --git a/spec/support/shared_examples/sign_in.rb b/spec/support/shared_examples/sign_in.rb
index 59b44b82b7a..042952d7b70 100644
--- a/spec/support/shared_examples/sign_in.rb
+++ b/spec/support/shared_examples/sign_in.rb
@@ -316,6 +316,56 @@ def user_with_broken_personal_key(scenario)
end
end
+RSpec.shared_examples 'logs reCAPTCHA event and redirects appropriately' do |successful_sign_in:|
+ it 'logs reCAPTCHA event and redirects to the correct location' do
+ visit new_user_session_path
+
+ asserted_expected_user = false
+ fake_analytics = FakeAnalytics.new
+ allow_any_instance_of(ApplicationController).to receive(:analytics).
+ and_wrap_original do |original|
+ original_analytics = original.call
+ if original_analytics.request.params[:controller] == 'users/sessions' &&
+ original_analytics.request.params[:action] == 'create'
+ expect(original_analytics.user).to eq(user)
+ asserted_expected_user = true
+ end
+
+ fake_analytics
+ end
+
+ fill_in :user_recaptcha_mock_score, with: '0.1'
+ fill_in_credentials_and_submit(user.email, user.password)
+ expect(asserted_expected_user).to eq(true)
+ expect(fake_analytics).to have_logged_event(
+ 'reCAPTCHA verify result received',
+ recaptcha_result: {
+ assessment_id: kind_of(String),
+ success: true,
+ score: 0.1,
+ errors: [],
+ reasons: [],
+ },
+ evaluated_as_valid: false,
+ score_threshold: 0.2,
+ form_class: 'RecaptchaMockForm',
+ )
+ expect(fake_analytics).to have_logged_event(
+ 'Email and Password Authentication',
+ hash_including(
+ success: successful_sign_in,
+ valid_captcha_result: false,
+ captcha_validation_performed: true,
+ ),
+ )
+ if successful_sign_in
+ expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms')
+ else
+ expect(current_path).to eq sign_in_security_check_failed_path
+ end
+ end
+end
+
def ial1_sign_in_with_personal_key_goes_to_sp(sp)
user = create_ial1_account_go_back_to_sp_and_sign_out(sp)
old_personal_key = PersonalKeyGenerator.new(user).generate!