<%= t(
'user_mailer.new_device_sign_in.help_html',
diff --git a/config/application.rb b/config/application.rb
index efb9e11c73f..a0c15a0c479 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -54,7 +54,7 @@ class Application < Rails::Application
end
end
- config.load_defaults '7.0'
+ config.load_defaults '7.1'
config.active_record.belongs_to_required_by_default = false
config.active_job.queue_adapter = :good_job
diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb
index 5016c76171e..4385cf76121 100644
--- a/config/initializers/rack_attack.rb
+++ b/config/initializers/rack_attack.rb
@@ -240,7 +240,7 @@ def headers
# If you want to return 503 so that the attacker might be fooled into
# believing that they've successfully broken your app (or you just want to
# customize the response), then uncomment these lines.
- self.throttled_response = lambda do |_env|
+ self.throttled_responder = lambda do |_env|
[
429, # status
{ 'Content-Type' => 'text/html' }, # headers
@@ -248,7 +248,7 @@ def headers
]
end
- self.blocklisted_response = throttled_response
+ self.blocklisted_responder = throttled_responder
end
end
diff --git a/config/locales/user_mailer/en.yml b/config/locales/user_mailer/en.yml
index 53126ff4332..9463cdf84bf 100644
--- a/config/locales/user_mailer/en.yml
+++ b/config/locales/user_mailer/en.yml
@@ -208,8 +208,7 @@ en:
help_html: If you did not make this change, %{disavowal_link_html}. For more
help, please visit the %{app_name_html} %{help_link_html} or
%{contact_link_html}.
- info_html: '
Your %{app_name} account was just used to sign in on a new
- device.
%{date} %{location}
'
+ info: 'Your %{app_name} account was just used to sign in on a new device.'
subject: New sign-in with your %{app_name} account
password_changed:
disavowal_link: reset your password
diff --git a/config/locales/user_mailer/es.yml b/config/locales/user_mailer/es.yml
index 9de0a63aa39..6f93f6dda26 100644
--- a/config/locales/user_mailer/es.yml
+++ b/config/locales/user_mailer/es.yml
@@ -222,8 +222,7 @@ es:
disavowal_link: restablecer su contraseña
help_html: Si no realizó este cambio, %{disavowal_link_html}. Para más ayuda,
visite el %{app_name_html} %{help_link_html} o el %{contact_link_html}.
- info_html: '
Su cuenta %{app_name} acaba de iniciar sesión en un nuevo
- dispositivo.
%{date} %{location}
'
+ info: 'Su cuenta %{app_name} acaba de iniciar sesión en un nuevo dispositivo.'
subject: Nuevo initio de sesion con su %{app_name} cuenta
password_changed:
disavowal_link: restablecer su contraseña
diff --git a/config/locales/user_mailer/fr.yml b/config/locales/user_mailer/fr.yml
index 39f1a5615b1..b0a9337a88a 100644
--- a/config/locales/user_mailer/fr.yml
+++ b/config/locales/user_mailer/fr.yml
@@ -228,8 +228,7 @@ fr:
help_html: Si vous n’avez pas effectué ce changement, %{disavowal_link_html}.
Pour plus d’aide, veuillez visiter le %{help_link_html} de
%{app_name_html} ou %{contact_link_html}.
- info_html: '
Votre compte %{app_name} a été connecté sur un nouvel
- appareil.
%{date} %{location}
'
+ info: 'Votre compte %{app_name} a été connecté sur un nouvel appareil.'
subject: Nouvelle connexion avec votre compte %{app_name}
password_changed:
disavowal_link: réinitialiser votre mot de passe
diff --git a/db/primary_migrate/20231204232215_add_idv_level_to_profile.rb b/db/primary_migrate/20231204232215_add_idv_level_to_profile.rb
new file mode 100644
index 00000000000..07a29f1d29e
--- /dev/null
+++ b/db/primary_migrate/20231204232215_add_idv_level_to_profile.rb
@@ -0,0 +1,5 @@
+class AddIdvLevelToProfile < ActiveRecord::Migration[7.1]
+ def change
+ add_column :profiles, :idv_level, :integer
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 789767c2929..ccc8c79c8d2 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: 2023_11_02_211426) do
+ActiveRecord::Schema[7.1].define(version: 2023_12_04_232215) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "pg_stat_statements"
@@ -457,6 +457,7 @@
t.text "encrypted_pii_multi_region"
t.text "encrypted_pii_recovery_multi_region"
t.datetime "gpo_verification_expired_at"
+ t.integer "idv_level"
t.index ["fraud_pending_reason"], name: "index_profiles_on_fraud_pending_reason"
t.index ["fraud_rejection_at"], name: "index_profiles_on_fraud_rejection_at"
t.index ["fraud_review_pending_at"], name: "index_profiles_on_fraud_review_pending_at"
diff --git a/lib/secure_cookies.rb b/lib/secure_cookies.rb
index b432ba90cce..72a2dc25d25 100644
--- a/lib/secure_cookies.rb
+++ b/lib/secure_cookies.rb
@@ -2,7 +2,6 @@
# Reimplements SecureHeaders secure cookie functionality to make sure all cookies are secure
class SecureCookies
- COOKIE_SEPARATOR = "\n"
SECURE_REGEX = /; Secure/i
HTTP_ONLY_REGEX = /; HttpOnly/i
SAME_SITE_REGEX = /; SameSite/i
@@ -13,19 +12,17 @@ def initialize(app)
def call(env)
status, headers, body = @app.call(env)
-
- if (cookie_header = headers['Set-Cookie']).present?
- cookies = cookie_header.split(COOKIE_SEPARATOR)
-
- cookies.each do |cookie|
- next if cookie.blank?
-
+ cookies = headers[Rack::SET_COOKIE]
+ if cookies
+ cookies = Array(cookies).map do |cookie|
cookie << '; Secure' if env['HTTPS'] == 'on' && !cookie.match?(SECURE_REGEX)
cookie << '; HttpOnly' if !cookie.match?(HTTP_ONLY_REGEX)
cookie << '; SameSite=Lax' if !cookie.match?(SAME_SITE_REGEX)
+
+ cookie
end
- headers['Set-Cookie'] = cookies.join(COOKIE_SEPARATOR)
+ headers[Rack::SET_COOKIE] = cookies
end
[status, headers, body]
diff --git a/spec/components/javascript_required_component_spec.rb b/spec/components/javascript_required_component_spec.rb
index b9e506f21c0..4f8a8bc1296 100644
--- a/spec/components/javascript_required_component_spec.rb
+++ b/spec/components/javascript_required_component_spec.rb
@@ -5,10 +5,11 @@
let(:header) { 'You must enable JavaScript' }
let(:intro) { nil }
+ let(:location) { 'example' }
let(:content) { 'JavaScript-required content' }
subject(:rendered) do
- render_inline described_class.new(header:, intro:).with_content(content)
+ render_inline described_class.new(header:, intro:, location:).with_content(content)
end
it 'renders instructions to enable JavaScript' do
@@ -25,7 +26,7 @@
end
it 'loads css resource for setting session key in JavaScript-disabled environments' do
- expect(rendered).to have_css("noscript link[href='#{no_js_detect_css_path}']")
+ expect(rendered).to have_css("noscript link[href='#{no_js_detect_css_path(location:)}']")
end
context 'with intro' do
@@ -49,7 +50,7 @@
it 'only renders the alert once' do
rendered
- second_rendered = render_inline described_class.new(header:)
+ second_rendered = render_inline described_class.new(header:, location:)
expect(second_rendered).not_to have_content(t('components.javascript_required.enabled_alert'))
end
diff --git a/spec/config/initializers/secure_headers_spec.rb b/spec/config/initializers/secure_headers_spec.rb
index 3695f20bcbb..d77094fdfe6 100644
--- a/spec/config/initializers/secure_headers_spec.rb
+++ b/spec/config/initializers/secure_headers_spec.rb
@@ -1,3 +1,5 @@
+require 'rails_helper'
+
RSpec.describe 'config.ssl_options' do
subject(:ssl_options) { Rails.application.config.ssl_options }
@@ -8,7 +10,7 @@
request = { 'HTTPS' => 'on' }
_status, headers, _body = ssl_middleware.call(request)
- expect(headers['Strict-Transport-Security']).
+ expect(headers['strict-transport-security']).
to eq('max-age=31556952; includeSubDomains; preload')
end
end
diff --git a/spec/controllers/idv/agreement_controller_spec.rb b/spec/controllers/idv/agreement_controller_spec.rb
index 16d51d7fd1b..dd13721b7e8 100644
--- a/spec/controllers/idv/agreement_controller_spec.rb
+++ b/spec/controllers/idv/agreement_controller_spec.rb
@@ -147,26 +147,91 @@
}.from(nil)
end
- it 'redirects to hybrid handoff' do
- put :update, params: params
- expect(response).to redirect_to(idv_hybrid_handoff_url)
- end
+ context 'on success' do
+ context 'skip_hybrid_handoff present in params' do
+ let(:skip_hybrid_handoff) { '' }
+
+ it 'sets flow_path to standard' do
+ expect do
+ put :update, params: params
+ end.to change {
+ subject.idv_session.flow_path
+ }.from(nil).to('standard').and change {
+ subject.idv_session.skip_hybrid_handoff
+ }.from(nil).to(true)
+ end
- context 'skip_hybrid_handoff present in params' do
- let(:skip_hybrid_handoff) { '' }
- it 'sets flow_path to standard' do
- expect do
+ it 'redirects to hybrid handoff' do
put :update, params: params
- end.to change {
- subject.idv_session.flow_path
- }.from(nil).to('standard').and change {
- subject.idv_session.skip_hybrid_handoff
- }.from(nil).to(true)
+
+ expect(response).to redirect_to(idv_hybrid_handoff_url)
+ end
+ end
+
+ context 'when both ipp and opt-in ipp are enabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { true }
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { true }
+ end
+
+ it 'redirects to how to verify' do
+ put :update, params: params
+ expect(response).to redirect_to(idv_how_to_verify_url)
+ end
+ end
+
+ context 'when ipp is enabled but opt-in ipp is disabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { true }
+ allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { false }
+ end
+
+ it 'redirects to hybrid handoff' do
+ put :update, params: params
+ expect(response).to redirect_to(idv_hybrid_handoff_url)
+ end
+ end
+
+ context 'when ipp is disabled and opt-in ipp is enabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { false }
+ allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { true }
+ end
+
+ it 'redirects to hybrid handoff' do
+ put :update, params: params
+ expect(response).to redirect_to(idv_hybrid_handoff_url)
+ end
+ end
+
+ context 'when both ipp and opt-in ipp are disabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { false }
+ allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { false }
+ end
+
+ it 'redirects to hybrid handoff' do
+ put :update, params: params
+ expect(response).to redirect_to(idv_hybrid_handoff_url)
+ end
+ end
+ end
+
+ context 'on failure' do
+ let(:skip_hybrid_handoff) { nil }
+
+ let(:params) do
+ {
+ doc_auth: {
+ idv_consent_given: nil,
+ },
+ skip_hybrid_handoff: skip_hybrid_handoff,
+ }.compact
end
- it 'redirects to hybrid handoff' do
+ it 'redirects to idv agreement' do
put :update, params: params
- expect(response).to redirect_to(idv_hybrid_handoff_url)
+ expect(response).to redirect_to(idv_agreement_url)
end
end
end
diff --git a/spec/controllers/idv/by_mail/enter_code_controller_spec.rb b/spec/controllers/idv/by_mail/enter_code_controller_spec.rb
index 9bc0a47e173..5cea7645e3c 100644
--- a/spec/controllers/idv/by_mail/enter_code_controller_spec.rb
+++ b/spec/controllers/idv/by_mail/enter_code_controller_spec.rb
@@ -1,69 +1,52 @@
require 'rails_helper'
RSpec.describe Idv::ByMail::EnterCodeController do
- let(:has_pending_profile) { true }
- let(:success) { true }
- let(:otp) { 'ABC123' }
- let(:submitted_otp) { otp }
- let(:user) { create(:user) }
- let(:profile_created_at) { Time.zone.now }
- let(:pending_profile) do
- if user
- create(
- :profile,
- :with_pii,
- user: user,
- proofing_components: proofing_components,
- created_at: profile_created_at,
- )
- end
- end
- let(:proofing_components) { nil }
+ let(:good_otp) { 'ABCDE12345' }
+ let(:bad_otp) { 'bad-otp' }
let(:threatmetrix_enabled) { false }
let(:gpo_enabled) { true }
+ let(:pii_cacher) { Pii::Cacher.new(user, controller.user_session) }
let(:params) { nil }
before do
stub_analytics
stub_attempts_tracker
+ stub_sign_in(user)
- if user
- stub_sign_in(user)
- pending_user = stub_user_with_pending_profile(user)
- creation_time =
- (IdentityConfig.store.minimum_wait_before_another_usps_letter_in_hours + 1).hours.ago
- create(
- :gpo_confirmation_code,
- profile: pending_profile,
- otp_fingerprint: Pii::Fingerprinter.fingerprint(otp),
- created_at: creation_time,
- updated_at: creation_time,
- )
- allow(pending_user).to receive(:gpo_verification_pending_profile?).
- and_return(has_pending_profile)
- end
-
+ allow(Pii::Cacher).to receive(:new).and_return(pii_cacher)
+ allow(pii_cacher).to receive(:fetch).and_call_original
+ allow(UserAlerts::AlertUserAboutAccountVerified).to receive(:call)
+ allow(@irs_attempts_api_tracker).to receive(:idv_gpo_verification_submitted)
allow(IdentityConfig.store).to receive(:proofing_device_profiling).
and_return(threatmetrix_enabled ? :enabled : :disabled)
allow(IdentityConfig.store).to receive(:enable_usps_verification).and_return(gpo_enabled)
end
describe '#index' do
- subject(:action) do
- get(:index, params: params)
- end
+ subject(:action) { get(:index, params: params) }
context 'user has pending profile' do
- it 'renders page' do
+ let(:profile_created_at) { 2.days.ago }
+ let(:user) { create(:user, :with_pending_gpo_profile, created_at: profile_created_at) }
+ let(:pending_profile) { user.gpo_verification_pending_profile }
+
+ before do
controller.user_session[:decrypted_pii] = { address1: 'Address1' }.to_json
- expect(@analytics).to receive(:track_event).with(
+ end
+
+ it 'renders page' do
+ action
+
+ expect(@analytics).to have_logged_event(
'IdV: enter verify by mail code visited',
source: nil,
)
+ expect(response).to render_template('idv/by_mail/enter_code/index')
+ end
+ it 'uses the PII from the pending profile' do
action
-
- expect(response).to render_template('idv/by_mail/enter_code/index')
+ expect(pii_cacher).to have_received(:fetch).with(pending_profile.id)
end
it 'sets @can_request_another_letter to true' do
@@ -71,16 +54,30 @@
expect(assigns(:can_request_another_letter)).to eql(true)
end
- it 'shows rate limited page if user is rate limited' do
- RateLimiter.new(rate_limit_type: :verify_gpo_key, user: user).increment_to_limited!
+ context 'when the user is rate limited' do
+ before do
+ RateLimiter.new(rate_limit_type: :verify_gpo_key, user: user).increment_to_limited!
+ end
- action
+ it 'shows rate limited page' do
+ action
+
+ expect(response).to redirect_to(idv_enter_code_rate_limited_url)
+ end
+
+ it 'logs an analytics event' do
+ action
- expect(response).to redirect_to(idv_enter_code_rate_limited_url)
+ expect(@analytics).to have_logged_event(
+ 'IdV: enter verify by mail code visited',
+ source: nil,
+ )
+ end
end
- context 'but that profile is > 30 days old' do
+ context 'but that profile is too old' do
let(:profile_created_at) { 31.days.ago }
+
it 'sets @can_request_another_letter to false' do
action
expect(assigns(:can_request_another_letter)).to eql(false)
@@ -88,15 +85,13 @@
end
context 'user clicked a "i did not receive my letter" link' do
- let(:params) do
- {
- did_not_receive_letter: 1,
- }
- end
+ let(:params) { { did_not_receive_letter: 1 } }
+
it 'sets @user_did_not_receive_letter to true' do
action
expect(assigns(:user_did_not_receive_letter)).to eql(true)
end
+
it 'augments analytics event' do
action
expect(@analytics).to have_logged_event(
@@ -107,59 +102,48 @@
end
end
- context 'user does not have pending profile' do
- let(:has_pending_profile) { false }
+ context 'user does not have a pending profile' do
+ let(:user) { create(:user) }
- it 'redirects to account page' do
+ it 'uses no PII' do
action
- expect(response).to redirect_to(account_url)
+ expect(pii_cacher).not_to have_received(:fetch)
end
- end
-
- context 'with rate limit reached' do
- before do
- RateLimiter.new(rate_limit_type: :verify_gpo_key, user: user).increment_to_limited!
- end
-
- it 'redirects to the rate limited page' do
- expect(@analytics).to receive(:track_event).with(
- 'IdV: enter verify by mail code visited',
- source: nil,
- ).once
+ it 'redirects to account page' do
action
- expect(response).to redirect_to(idv_enter_code_rate_limited_url)
+ expect(response).to redirect_to(account_url)
end
end
context 'session says user did not receive letter' do
+ let(:user) { create(:user, :with_pending_gpo_profile, created_at: 2.days.ago) }
+
before do
session[:gpo_user_did_not_receive_letter] = true
- action
end
+
it 'redirects user to url with querystring' do
+ action
expect(response).to redirect_to(
idv_verify_by_mail_enter_code_path(did_not_receive_letter: 1),
)
end
+
it 'clears session value' do
+ action
expect(session).not_to include(gpo_user_did_not_receive_letter: anything)
end
end
- context 'querystring says user did not receive letter' do
- let(:params) do
- { did_not_receive_letter: 1 }
- end
-
- context 'not logged in' do
- let(:user) { nil }
+ context 'not logged in, and querystring says user did not receive letter' do
+ let(:user) { nil }
+ let(:params) { { did_not_receive_letter: 1 } }
- it 'sets value in session' do
- expect { action }.to change { session[:gpo_user_did_not_receive_letter ] }.to eql(true)
- end
+ it 'sets value in session' do
+ expect { action }.to change { session[:gpo_user_did_not_receive_letter ] }.to eql(true)
end
end
end
@@ -168,22 +152,38 @@
let(:otp_code_error_message) { { otp: [t('errors.messages.confirmation_code_incorrect')] } }
let(:success_properties) { { success: true } }
- subject(:action) do
- post(
- :create,
- params: {
- gpo_verify_form: {
- otp: submitted_otp,
- },
- },
- )
+ context 'user does not have a pending profile' do
+ let(:user) { create(:user, :fully_registered) }
+
+ it 'uses no PII' do
+ expect(pii_cacher).not_to have_received(:fetch)
+ end
end
context 'with a valid form' do
+ subject(:action) do
+ post(:create, params: { gpo_verify_form: { otp: good_otp } })
+ end
+
+ let(:user) { create(:user, :with_pending_gpo_profile, created_at: 2.days.ago) }
+ let(:pending_profile) { user.gpo_verification_pending_profile }
let(:success) { true }
+ it 'uses the PII from the pending profile' do
+ # action will make the profile active, so grab the ID here.
+ pending_profile_id = pending_profile.id
+
+ action
+ expect(pii_cacher).to have_received(:fetch).with(pending_profile_id)
+ end
+
it 'redirects to the sign_up/completions page' do
- expect(@analytics).to receive(:track_event).with(
+ action
+
+ expect(@irs_attempts_api_tracker).to have_received(:idv_gpo_verification_submitted).
+ with(success_properties)
+
+ expect(@analytics).to have_logged_event(
'IdV: enter verify by mail code submitted',
success: true,
errors: {},
@@ -193,13 +193,7 @@
which_letter: 1,
letter_count: 1,
attempts: 1,
- pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
)
- expect(@irs_attempts_api_tracker).to receive(:idv_gpo_verification_submitted).
- with(success_properties)
-
- action
-
event_count = user.events.where(event_type: :account_verified, ip: '0.0.0.0').
where(disavowal_token_fingerprint: nil).count
expect(event_count).to eq 1
@@ -207,9 +201,9 @@
end
it 'dispatches account verified alert' do
- expect(UserAlerts::AlertUserAboutAccountVerified).to receive(:call)
-
action
+
+ expect(UserAlerts::AlertUserAboutAccountVerified).to have_received(:call)
end
context 'with establishing in person enrollment' do
@@ -218,14 +212,10 @@
:in_person_enrollment,
:pending,
user: user,
- profile: pending_profile,
+ profile: user.pending_profile,
)
end
- let(:proofing_components) do
- ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS)
- end
-
before do
allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
allow(controller).to receive(:pii).
@@ -233,7 +223,12 @@
end
it 'redirects to personal key page' do
- expect(@analytics).to receive(:track_event).with(
+ action
+
+ expect(@irs_attempts_api_tracker).to have_received(:idv_gpo_verification_submitted).
+ with(success_properties)
+
+ expect(@analytics).to have_logged_event(
'IdV: enter verify by mail code submitted',
success: true,
errors: {},
@@ -243,36 +238,28 @@
which_letter: 1,
letter_count: 1,
attempts: 1,
- pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
)
- expect(@irs_attempts_api_tracker).to receive(:idv_gpo_verification_submitted).
- with(success_properties)
-
- action
-
expect(response).to redirect_to(idv_personal_key_url)
end
it 'does not dispatch account verified alert' do
- expect(UserAlerts::AlertUserAboutAccountVerified).not_to receive(:call)
-
action
+
+ expect(UserAlerts::AlertUserAboutAccountVerified).not_to have_received(:call)
end
end
context 'threatmetrix disabled' do
context 'with threatmetrix status of "reject"' do
- let(:pending_profile) do
- create(
- :profile,
- :with_pii,
- fraud_pending_reason: 'threatmetrix_reject',
- user: user,
- )
- end
+ let(:user) { create(:user, :gpo_pending_with_fraud_rejection) }
it 'redirects to the sign_up/completions page' do
- expect(@analytics).to receive(:track_event).with(
+ action
+
+ expect(@irs_attempts_api_tracker).to have_received(:idv_gpo_verification_submitted).
+ with(success_properties)
+
+ expect(@analytics).to have_logged_event(
'IdV: enter verify by mail code submitted',
success: true,
errors: {},
@@ -282,13 +269,7 @@
which_letter: 1,
letter_count: 1,
attempts: 1,
- pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
)
- expect(@irs_attempts_api_tracker).to receive(:idv_gpo_verification_submitted).
- with(success_properties)
-
- action
-
event_count = user.events.where(event_type: :account_verified, ip: '0.0.0.0').
where(disavowal_token_fingerprint: nil).count
expect(event_count).to eq 1
@@ -301,17 +282,12 @@
let(:threatmetrix_enabled) { true }
context 'with threatmetrix status of "reject"' do
- let(:pending_profile) do
- create(
- :profile,
- :with_pii,
- fraud_pending_reason: 'threatmetrix_reject',
- user: user,
- )
- end
+ let(:user) { create(:user, :gpo_pending_with_fraud_rejection) }
it 'is reflected in analytics' do
- expect(@analytics).to receive(:track_event).with(
+ action
+
+ expect(@analytics).to have_logged_event(
'IdV: enter verify by mail code submitted',
success: true,
errors: {},
@@ -321,37 +297,30 @@
which_letter: 1,
letter_count: 1,
attempts: 1,
- pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
)
- action
-
expect(response).to redirect_to(idv_personal_key_url)
end
it 'does not show a flash message' do
- expect(flash[:success]).to be_nil
action
+ expect(flash[:success]).to be_nil
end
it 'does not dispatch account verified alert' do
- expect(UserAlerts::AlertUserAboutAccountVerified).not_to receive(:call)
action
+
+ expect(UserAlerts::AlertUserAboutAccountVerified).not_to have_received(:call)
end
end
context 'with threatmetrix status of "review"' do
- let(:pending_profile) do
- create(
- :profile,
- :with_pii,
- fraud_pending_reason: 'threatmetrix_review',
- user: user,
- )
- end
+ let(:user) { create(:user, :gpo_pending_with_fraud_review) }
it 'is reflected in analytics' do
- expect(@analytics).to receive(:track_event).with(
+ action
+
+ expect(@analytics).to have_logged_event(
'IdV: enter verify by mail code submitted',
success: true,
errors: {},
@@ -361,11 +330,8 @@
which_letter: 1,
letter_count: 1,
attempts: 1,
- pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
)
- action
-
expect(response).to redirect_to(idv_personal_key_url)
end
end
@@ -373,10 +339,19 @@
end
context 'with an invalid form' do
- let(:submitted_otp) { 'the-wrong-otp' }
+ subject(:action) do
+ post(:create, params: { gpo_verify_form: { otp: bad_otp } })
+ end
+
+ let(:user) { create(:user, :with_pending_gpo_profile, created_at: 2.days.ago) }
it 'redirects to the index page to show errors' do
- expect(@analytics).to receive(:track_event).with(
+ action
+
+ expect(@irs_attempts_api_tracker).to have_received(:idv_gpo_verification_submitted).
+ with(success: false)
+
+ expect(@analytics).to have_logged_event(
'IdV: enter verify by mail code submitted',
success: false,
errors: otp_code_error_message,
@@ -387,33 +362,30 @@
letter_count: 1,
attempts: 1,
error_details: { otp: { confirmation_code_incorrect: true } },
- pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
)
- expect(@irs_attempts_api_tracker).to receive(:idv_gpo_verification_submitted).
- with(success: false)
-
- action
-
expect(response).to redirect_to(idv_verify_by_mail_enter_code_url)
end
it 'does not 500 with missing form keys' do
- expect { post(:create, params: { otp: submitted_otp }) }.to raise_exception(
+ expect { post(:create, params: {}) }.to raise_exception(
ActionController::ParameterMissing,
)
end
end
context 'final attempt before rate limited' do
- let(:invalid_otp) { 'a-wrong-otp' }
+ let(:user) { create(:user, :with_pending_gpo_profile) }
let(:max_attempts) { 2 }
before do
allow(IdentityConfig.store).to receive(:verify_gpo_key_max_attempts).
and_return(max_attempts)
+ (max_attempts - 1).times do |i|
+ post(:create, params: { gpo_verify_form: { otp: bad_otp } })
+ end
end
- context 'user is rate limited' do
+ context 'invalid code is submitted' do
it 'redirects to the rate limited index page to show errors' do
analytics_args = {
success: false,
@@ -425,36 +397,19 @@
letter_count: 1,
attempts: 1,
error_details: { otp: { confirmation_code_incorrect: true } },
- pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
}
- expect(@analytics).to receive(:track_event).with(
+ post(:create, params: { gpo_verify_form: { otp: bad_otp } })
+
+ expect(@analytics).to have_logged_event(
'IdV: enter verify by mail code submitted',
**analytics_args,
- ).once
+ )
+
analytics_args[:attempts] = 2
- expect(@analytics).to receive(:track_event).with(
+
+ expect(@analytics).to have_logged_event(
'IdV: enter verify by mail code submitted',
**analytics_args,
- ).once
-
- max_attempts.times do |i|
- post(
- :create,
- params: {
- gpo_verify_form: {
- otp: invalid_otp,
- },
- },
- )
- end
-
- post(
- :create,
- params: {
- gpo_verify_form: {
- otp: submitted_otp,
- },
- },
)
expect(response).to redirect_to(idv_enter_code_rate_limited_url)
@@ -462,55 +417,24 @@
end
context 'valid code is submitted' do
+ let(:user) { create(:user, :with_pending_gpo_profile) }
+
it 'redirects to personal key page' do
- expect(@analytics).to receive(:track_event).with(
- 'IdV: enter verify by mail code submitted',
- success: false,
- errors: otp_code_error_message,
- pending_in_person_enrollment: false,
- fraud_check_failed: false,
- enqueued_at: nil,
- which_letter: nil,
- letter_count: 1,
- attempts: 1,
- error_details: { otp: { confirmation_code_incorrect: true } },
- pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
- ).exactly(max_attempts - 1).times
- expect(@analytics).to receive(:track_event).with(
- 'IdV: enter verify by mail code submitted',
- success: true,
- errors: {},
- pending_in_person_enrollment: false,
- fraud_check_failed: false,
- enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at,
- which_letter: 1,
- letter_count: 1,
- attempts: 2,
- pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
- ).once
- expect(@irs_attempts_api_tracker).to receive(:idv_gpo_verification_submitted).
+ post(:create, params: { gpo_verify_form: { otp: good_otp } })
+
+ expect(@irs_attempts_api_tracker).to have_received(:idv_gpo_verification_submitted).
exactly(max_attempts).times
- (max_attempts - 1).times do |i|
- post(
- :create,
- params: {
- gpo_verify_form: {
- otp: invalid_otp,
- },
- },
- )
- end
+ failed_gpo_submission_events =
+ @analytics.events['IdV: enter verify by mail code submitted'].
+ reject { |event_attributes| event_attributes[:errors].empty? }
- post(
- :create,
- params: {
- gpo_verify_form: {
- otp: submitted_otp,
- },
- },
- )
+ successful_gpo_submission_events =
+ @analytics.events['IdV: enter verify by mail code submitted'].
+ select { |event_attributes| event_attributes[:errors].empty? }
+ expect(failed_gpo_submission_events.count).to eq(max_attempts - 1)
+ expect(successful_gpo_submission_events.count).to eq(1)
expect(response).to redirect_to(idv_personal_key_url)
end
end
diff --git a/spec/controllers/idv/how_to_verify_controller_spec.rb b/spec/controllers/idv/how_to_verify_controller_spec.rb
index d52d7268a53..6d3c2c72923 100644
--- a/spec/controllers/idv/how_to_verify_controller_spec.rb
+++ b/spec/controllers/idv/how_to_verify_controller_spec.rb
@@ -3,21 +3,21 @@
RSpec.describe Idv::HowToVerifyController do
let(:user) { create(:user) }
let(:enabled) { true }
+ let(:ab_test_args) do
+ { sample_bucket1: :sample_value1, sample_bucket2: :sample_value2 }
+ end
before do
- allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { enabled }
+ allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { true }
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { true }
stub_sign_in(user)
stub_analytics
+ allow(@analytics).to receive(:track_event)
+ allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args)
subject.idv_session.welcome_visited = true
subject.idv_session.idv_consent_given = true
end
- describe '#step_info' do
- it 'returns a valid StepInfo object' do
- expect(Idv::HowToVerifyController.step_info).to be_valid
- end
- end
-
describe 'before_actions' do
it 'includes authentication before_action' do
expect(subject).to have_actions(
@@ -25,9 +25,75 @@
:confirm_two_factor_authenticated,
)
end
+
+ context 'confirm_step_allowed' do
+ context 'when ipp is disabled and opt-in ipp is enabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { false }
+ allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { true }
+ end
+
+ it 'disables the how to verify step and redirects to hybrid handoff' do
+ get :show
+
+ expect(Idv::HowToVerifyController.enabled?).to be false
+ expect(subject.idv_session.skip_doc_auth).to be_nil
+ expect(response).to redirect_to(idv_hybrid_handoff_url)
+ end
+ end
+
+ context 'when ipp is enabled but opt-in ipp is disabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { true }
+ allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { false }
+ end
+
+ it 'disables the how to verify step and redirects to hybrid handoff' do
+ get :show
+
+ expect(Idv::HowToVerifyController.enabled?).to be false
+ expect(subject.idv_session.skip_doc_auth).to be_nil
+ expect(response).to redirect_to(idv_hybrid_handoff_url)
+ end
+ end
+
+ context 'when both ipp and opt-in ipp are disabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { false }
+ allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { false }
+ end
+
+ it 'disables the how to verify step and redirects to hybrid handoff' do
+ get :show
+
+ expect(Idv::HowToVerifyController.enabled?).to be false
+ expect(subject.idv_session.skip_doc_auth).to be_nil
+ expect(response).to redirect_to(idv_hybrid_handoff_url)
+ end
+ end
+
+ context 'when both ipp and opt-in ipp are enabled' do
+ it 'renders the show template for how to verify' do
+ get :show
+
+ expect(Idv::HowToVerifyController.enabled?).to be true
+ expect(subject.idv_session.skip_doc_auth).to be_nil
+ expect(response).to render_template :show
+ end
+ end
+ end
end
describe '#show' do
+ let(:analytics_name) { :idv_doc_auth_how_to_verify_visited }
+ let(:analytics_args) do
+ {
+ step: 'how_to_verify',
+ analytics_id: 'Doc Auth',
+ skip_hybrid_handoff: nil,
+ irs_reproofing: false,
+ }.merge(ab_test_args)
+ end
it 'renders the show template' do
get :show
@@ -35,6 +101,12 @@
expect(response).to render_template :show
end
+ it 'sends analytics_visited event' do
+ get :show
+
+ expect(@analytics).to have_received(:track_event).with(analytics_name, analytics_args)
+ end
+
context 'agreement step not completed' do
before do
subject.idv_session.idv_consent_given = nil
@@ -54,32 +126,85 @@
idv_how_to_verify_form: { selection: selection },
}
end
- let(:selection) { 'remote' }
+ let(:analytics_name) { :idv_doc_auth_how_to_verify_submitted }
+ context 'no selection made' do
+ let(:analytics_args) do
+ {
+ step: 'how_to_verify',
+ analytics_id: 'Doc Auth',
+ skip_hybrid_handoff: nil,
+ irs_reproofing: false,
+ error_details: { selection: { blank: true } },
+ errors: { selection: ['Select a way to verify your identity.'] },
+ success: false,
+ }.merge(ab_test_args)
+ end
+
+ it 'invalidates future steps' do
+ expect(subject).to receive(:clear_future_steps!)
+
+ put :update
+ end
- it 'invalidates future steps' do
- expect(subject).to receive(:clear_future_steps!)
+ it 'sends analytics_submitted event when nothing is selected' do
+ put :update
- put :update
+ expect(@analytics).to have_received(:track_event).with(analytics_name, analytics_args)
+ end
end
context 'remote' do
+ let(:selection) { 'remote' }
+ let(:analytics_args) do
+ {
+ analytics_id: 'Doc Auth',
+ skip_hybrid_handoff: nil,
+ step: 'how_to_verify',
+ irs_reproofing: false,
+ errors: {},
+ success: true,
+ 'selection' => selection,
+ }.merge(ab_test_args)
+ end
it 'sets skip doc auth on idv session to false and redirects to hybrid handoff' do
put :update, params: params
expect(subject.idv_session.skip_doc_auth).to be false
expect(response).to redirect_to(idv_hybrid_handoff_url)
end
+
+ it 'sends analytics_submitted event when remote proofing is selected' do
+ put :update, params: params
+
+ expect(@analytics).to have_received(:track_event).with(analytics_name, analytics_args)
+ end
end
context 'ipp' do
let(:selection) { 'ipp' }
-
+ let(:analytics_args) do
+ {
+ analytics_id: 'Doc Auth',
+ skip_hybrid_handoff: nil,
+ step: 'how_to_verify',
+ irs_reproofing: false,
+ errors: {},
+ success: true,
+ 'selection' => selection,
+ }.merge(ab_test_args)
+ end
it 'sets skip doc auth on idv session to true and redirects to document capture' do
put :update, params: params
expect(subject.idv_session.skip_doc_auth).to be true
expect(response).to redirect_to(idv_document_capture_url)
end
+
+ it 'sends analytics_submitted event when remote proofing is selected' do
+ put :update, params: params
+
+ expect(@analytics).to have_received(:track_event).with(analytics_name, analytics_args)
+ end
end
context 'undo/back' do
@@ -91,4 +216,10 @@
end
end
end
+
+ describe '#step_info' do
+ it 'returns a valid StepInfo object' do
+ expect(Idv::HowToVerifyController.step_info).to be_valid
+ end
+ end
end
diff --git a/spec/controllers/idv/phone_controller_spec.rb b/spec/controllers/idv/phone_controller_spec.rb
index e29505e7a70..1be321178aa 100644
--- a/spec/controllers/idv/phone_controller_spec.rb
+++ b/spec/controllers/idv/phone_controller_spec.rb
@@ -274,6 +274,16 @@
expect(response).to render_template(:new)
end
+ it 'invalidates phone step in idv_session' do
+ subject.idv_session.vendor_phone_confirmation = true
+ subject.idv_session.user_phone_confirmation = true
+
+ put :create, params: improbable_phone_form
+
+ expect(subject.idv_session.vendor_phone_confirmation).to be_nil
+ expect(subject.idv_session.user_phone_confirmation).to be_nil
+ end
+
it 'disallows non-US numbers' do
put :create, params: { idv_phone_form: { phone: international_phone } }
@@ -320,25 +330,31 @@
end
context 'when form is valid' do
+ let(:phone_params) do
+ { idv_phone_form: {
+ phone: good_phone,
+ otp_delivery_preference: :sms,
+ } }
+ end
+
before do
stub_analytics
stub_attempts_tracker
allow(@analytics).to receive(:track_event)
end
- it 'invalidates future steps' do
+ it 'invalidates future steps and invalidates phone step' do
user = build(:user, :with_phone, with: { phone: good_phone, confirmed_at: Time.zone.now })
stub_verify_steps_one_and_two(user)
- expect(subject).to receive(:clear_future_steps!)
+ subject.idv_session.vendor_phone_confirmation = true
+ subject.idv_session.user_phone_confirmation = true
- phone_params = {
- idv_phone_form: {
- phone: good_phone,
- otp_delivery_preference: :sms,
- },
- }
+ expect(subject).to receive(:clear_future_steps!)
put :create, params: phone_params
+
+ expect(subject.idv_session.vendor_phone_confirmation).to be_nil
+ expect(subject.idv_session.user_phone_confirmation).to be_nil
end
it 'tracks events with valid phone' do
@@ -350,13 +366,6 @@
phone_number: good_phone,
)
- phone_params = {
- idv_phone_form: {
- phone: good_phone,
- otp_delivery_preference: :sms,
- },
- }
-
put :create, params: phone_params
result = {
@@ -403,12 +412,7 @@
it 'redirects to otp delivery page' do
original_applicant = subject.idv_session.applicant.dup
- put :create, params: {
- idv_phone_form: {
- phone: good_phone,
- otp_delivery_preference: 'sms',
- },
- }
+ put :create, params: phone_params
expect(response).to redirect_to idv_phone_path
get :new
@@ -452,12 +456,7 @@
end
it 'redirects to otp page and does not set phone_confirmed_at' do
- put :create, params: {
- idv_phone_form: {
- phone: good_phone,
- otp_delivery_preference: 'sms',
- },
- }
+ put :create, params: phone_params
expect(response).to redirect_to idv_phone_path
get :new
diff --git a/spec/controllers/idv/phone_errors_controller_spec.rb b/spec/controllers/idv/phone_errors_controller_spec.rb
index eb9a367e6d6..697d8326044 100644
--- a/spec/controllers/idv/phone_errors_controller_spec.rb
+++ b/spec/controllers/idv/phone_errors_controller_spec.rb
@@ -5,6 +5,12 @@
{ sample_bucket1: :sample_value1, sample_bucket2: :sample_value2 }
end
+ describe '#step_info' do
+ it 'returns a valid StepInfo object' do
+ expect(Idv::PhoneErrorsController.step_info).to be_valid
+ end
+ end
+
before do
allow(subject).to receive(:remaining_attempts).and_return(5)
stub_analytics
@@ -13,6 +19,13 @@
if user
stub_sign_in(user)
+ subject.idv_session.welcome_visited = true
+ subject.idv_session.idv_consent_given = true
+ subject.idv_session.flow_path = 'standard'
+ subject.idv_session.pii_from_doc = Idp::Constants::MOCK_IDV_APPLICANT
+ subject.idv_session.ssn = '123-45-6789'
+ subject.idv_session.applicant = Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE
+ subject.idv_session.resolution_successful = true
subject.idv_session.user_phone_confirmation = false
subject.idv_session.previous_phone_step_params = previous_phone_step_params
end
@@ -28,7 +41,7 @@
context 'authenticated user' do
let(:user) { create(:user) }
- context 'the user has not submtted a phone number' do
+ context 'the user has not submitted a phone number' do
it 'redirects to phone step' do
subject.idv_session.previous_phone_step_params = nil
get action
@@ -78,19 +91,10 @@
subject.idv_session.user_phone_confirmation = true
end
- it 'redirects to the review url' do
+ it 'allows the back button and renders the template' do
get action
- expect(response).to redirect_to(idv_enter_password_url)
- end
- it 'does not log an event' do
- expect(@analytics).not_to receive(:track_event).with(
- 'IdV: phone error visited',
- hash_including(
- type: action,
- ),
- )
- get action
+ expect(response).to render_template(template)
end
end
end
diff --git a/spec/controllers/no_js_controller_spec.rb b/spec/controllers/no_js_controller_spec.rb
index ba3a3b1c53e..1578e7d0796 100644
--- a/spec/controllers/no_js_controller_spec.rb
+++ b/spec/controllers/no_js_controller_spec.rb
@@ -2,7 +2,12 @@
RSpec.describe NoJsController do
describe '#index' do
- subject(:response) { get :index }
+ let(:location) { 'example' }
+ subject(:response) { get :index, params: { location: } }
+
+ before do
+ stub_analytics
+ end
it 'returns empty css' do
expect(response.content_type.split(';').first).to eq('text/css')
@@ -14,5 +19,11 @@
expect(session[NoJsController::SESSION_KEY]).to eq(true)
end
+
+ it 'logs an event' do
+ response
+
+ expect(@analytics).to have_logged_event(:no_js_detect_stylesheet_loaded, location:)
+ end
end
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 2782b012c0c..5a625a1ef05 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -215,11 +215,14 @@
:with_pii,
gpo_verification_pending_at: context.code_sent_at,
user: user,
- created_at: context.created_at,
+ created_at: context.code_sent_at,
+ updated_at: context.code_sent_at,
)
create(
:gpo_confirmation_code,
profile: profile,
+ created_at: context.code_sent_at,
+ updated_at: context.code_sent_at,
code_sent_at: context.code_sent_at,
)
create(
@@ -228,6 +231,7 @@
device: create(:device, user: user),
event_type: :gpo_mail_sent,
created_at: context.code_sent_at,
+ updated_at: context.code_sent_at,
)
end
end
@@ -257,6 +261,22 @@
end
end
+ trait :gpo_pending_with_fraud_rejection do
+ with_pending_gpo_profile
+ after :create do |user|
+ user.pending_profile.fraud_rejection_at = 15.days.ago
+ user.pending_profile.fraud_pending_reason = :threatmetrix_reject
+ end
+ end
+
+ trait :gpo_pending_with_fraud_review do
+ with_pending_gpo_profile
+ after :create do |user|
+ user.pending_profile.fraud_review_pending_at = 15.days.ago
+ user.pending_profile.fraud_pending_reason = :threatmetrix_review
+ end
+ end
+
trait :fraud_rejection do
fully_registered
diff --git a/spec/features/idv/doc_auth/how_to_verify_spec.rb b/spec/features/idv/doc_auth/how_to_verify_spec.rb
index e6ef8c4701c..532f0319c6d 100644
--- a/spec/features/idv/doc_auth/how_to_verify_spec.rb
+++ b/spec/features/idv/doc_auth/how_to_verify_spec.rb
@@ -4,25 +4,61 @@
include IdvHelper
include DocAuthHelper
- let(:enabled) { true }
+ context 'when ipp is enabled and opt-in ipp is disabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { true }
+ allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { false }
- before do
- allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { enabled }
+ sign_in_and_2fa_user
+ complete_doc_auth_steps_before_agreement_step
+ complete_agreement_step
+ end
- sign_in_and_2fa_user
- complete_doc_auth_steps_before_agreement_step
- complete_agreement_step
+ it 'skips when disabled and redirects to hybird handoff)' do
+ expect(page).to have_current_path(idv_hybrid_handoff_url)
+ end
end
- context 'opt-in ipp is turned off' do
- let(:enabled) { false }
+ context 'when ipp is disabled and opt-in ipp is enabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { false }
+ allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { true }
+
+ sign_in_and_2fa_user
+ complete_doc_auth_steps_before_agreement_step
+ complete_agreement_step
+ end
- it 'skips when disabled' do
+ it 'skips when disabled and redirects to hybird handoff' do
expect(page).to have_current_path(idv_hybrid_handoff_url)
end
end
- context 'opt-in ipp is turned on' do
+ context 'when both ipp and opt-in ipp are disabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { false }
+ allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { false }
+
+ sign_in_and_2fa_user
+ complete_doc_auth_steps_before_agreement_step
+ complete_agreement_step
+ end
+
+ it 'skips when disabled and redirects to hybird handoff' do
+ expect(page).to have_current_path(idv_hybrid_handoff_url)
+ end
+ end
+
+ context 'when both ipp and opt-in ipp are enabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { true }
+ allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { true }
+
+ sign_in_and_2fa_user
+ complete_doc_auth_steps_before_agreement_step
+ complete_agreement_step
+ end
+
it 'displays expected content and requires a choice' do
expect(page).to have_current_path(idv_how_to_verify_path)
diff --git a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb
index 4e8a1d3b3bd..13cb54461f4 100644
--- a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb
+++ b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb
@@ -268,7 +268,10 @@
end
it 'regenerates backup codes path if a user clicks that they need new backup codes' do
- click_link strip_tags(t('two_factor_authentication.backup_codes.new_backup_codes_html'))
+ find(
+ 'a',
+ text: t('two_factor_authentication.backup_codes.new_backup_codes_html').gsub(' ', ' '),
+ ).click
expect(page).to have_current_path backup_code_regenerate_path
end
end
diff --git a/spec/features/webauthn/hidden_spec.rb b/spec/features/webauthn/hidden_spec.rb
index 28ba155b4bc..7567ee11224 100644
--- a/spec/features/webauthn/hidden_spec.rb
+++ b/spec/features/webauthn/hidden_spec.rb
@@ -2,6 +2,7 @@
RSpec.describe 'webauthn hide' do
include JavascriptDriverHelper
+ include WebAuthnHelper
describe 'security key' do
let(:option_id) { 'two_factor_options_form_selection_webauthn' }
@@ -58,9 +59,11 @@
expect(webauthn_option_hidden?).to eq(true)
end
- context 'with supported browser', driver: :headless_chrome_mobile do
+ context 'with supported browser and platform authenticator available',
+ driver: :headless_chrome_mobile do
it 'displays the authenticator option' do
sign_up_and_set_password
+ simulate_platform_authenticator_available
expect(webauthn_option_hidden?).to eq(false)
end
diff --git a/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx b/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx
index 28fd65c4487..a542fd4c00f 100644
--- a/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx
+++ b/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx
@@ -1113,6 +1113,25 @@ describe('document-capture/components/acuant-capture', () => {
});
});
+ context('mobile selfie', () => {
+ it('renders the selfie capture loading div in acuant-capture', async () => {
+ // What we want to test is that the selfie version of the FileInput appears
+ // when the name="selfie". The only difference between the selfie and document
+ // versions is what happens when you click the FileInput, so this test clicks
+ // the file input, then checks that the full screen div opened
+ const { getByRole, getByLabelText } = render(
+
+
+
+
+ ,
+ );
+
+ await userEvent.click(getByLabelText('Image'));
+ expect(getByRole('dialog')).to.be.ok();
+ });
+ });
+
it('optionally disallows upload', () => {
const { getByText } = render(
diff --git a/spec/javascript/packages/document-capture/components/acuant-selfie-camera-spec.jsx b/spec/javascript/packages/document-capture/components/acuant-selfie-camera-spec.jsx
new file mode 100644
index 00000000000..d392b21026c
--- /dev/null
+++ b/spec/javascript/packages/document-capture/components/acuant-selfie-camera-spec.jsx
@@ -0,0 +1,68 @@
+import { AcuantContextProvider, DeviceContext } from '@18f/identity-document-capture';
+import AcuantSelfieCamera from '@18f/identity-document-capture/components/acuant-selfie-camera';
+import AcuantSelfieCaptureCanvas from '@18f/identity-document-capture/components/acuant-selfie-capture-canvas';
+import { render, useAcuant } from '../../../support/document-capture';
+
+describe('document-capture/components/acuant-selfie-camera', () => {
+ const { initialize } = useAcuant();
+
+ it('waits for initialization', () => {
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+ // At this point, it's assumed `window.AcuantPassivelivenss.start` has not been called. This can't be
+ // asserted, since the global is only assigned as part of `initialize` itself. But we can rely
+ // on the fact that if it was called, an error would be thrown, and the test would fail.
+
+ initialize();
+
+ expect(window.AcuantPassiveLiveness.start.calledOnce).to.be.true();
+
+ const callbacks = window.AcuantPassiveLiveness.start.getCall(0).args[0];
+ const callbackNames = Object.keys(callbacks).sort;
+ const expectedCallbackNames = [
+ 'onDetectorInitialized',
+ 'onDetection',
+ 'onOpened',
+ 'onClosed',
+ 'onError',
+ 'onPhotoTaken',
+ 'onPhotoRetake',
+ 'onCaptured',
+ ].sort;
+ expect(callbackNames).to.equal(expectedCallbackNames);
+
+ expect(window.AcuantPassiveLiveness.start.getCall(0).args[1]).to.deep.equal({
+ FACE_NOT_FOUND: 'FACE NOT FOUND',
+ TOO_MANY_FACES: 'TOO MANY FACES',
+ FACE_ANGLE_TOO_LARGE: 'FACE ANGLE TOO LARGE',
+ PROBABILITY_TOO_SMALL: 'PROBABILITY TOO SMALL',
+ FACE_TOO_SMALL: 'FACE TOO SMALL',
+ FACE_CLOSE_TO_BORDER: 'TOO CLOSE TO THE FRAME',
+ });
+ });
+
+ it('ends on unmount', () => {
+ const { unmount } = render(
+
+
+
+
+
+
+ ,
+ );
+
+ initialize();
+ unmount();
+
+ expect(window.AcuantPassiveLiveness.end.calledOnce).to.be.true();
+ });
+});
diff --git a/spec/javascript/packages/document-capture/components/acuant-selfie-capture-canvas-spec.jsx b/spec/javascript/packages/document-capture/components/acuant-selfie-capture-canvas-spec.jsx
new file mode 100644
index 00000000000..3558b5a2b7e
--- /dev/null
+++ b/spec/javascript/packages/document-capture/components/acuant-selfie-capture-canvas-spec.jsx
@@ -0,0 +1,29 @@
+import AcuantSelfieCaptureCanvas from '@18f/identity-document-capture/components/acuant-selfie-capture-canvas';
+import { AcuantContext, DeviceContext } from '@18f/identity-document-capture';
+import { render } from '../../../support/document-capture';
+
+it('shows the loading spinner when the script hasnt loaded', () => {
+ const { getByRole, container } = render(
+
+
+
+
+ ,
+ );
+
+ expect(getByRole('dialog')).to.be.ok();
+ expect(container.querySelector('#acuant-face-capture-container')).to.not.exist();
+});
+
+it('shows the Acuant div when the script has loaded', () => {
+ const { queryByRole, container } = render(
+
+
+
+
+ ,
+ );
+
+ expect(queryByRole('dialog')).to.not.exist();
+ expect(container.querySelector('#acuant-face-capture-container')).to.exist();
+});
diff --git a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx b/spec/javascript/packages/document-capture/components/documents-step-spec.jsx
index 62e4b5a169c..c54dcc0b1be 100644
--- a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx
+++ b/spec/javascript/packages/document-capture/components/documents-step-spec.jsx
@@ -15,14 +15,16 @@ import { render } from '../../../support/document-capture';
import { getFixtureFile } from '../../../support/file';
describe('document-capture/components/documents-step', () => {
- it('renders with front and back inputs', () => {
- const { getByLabelText } = render();
+ it('renders with only front and back inputs by default', () => {
+ const { 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');
expect(front).to.be.ok();
expect(back).to.be.ok();
+ expect(selfie).to.not.exist();
});
it('calls onChange callback with uploaded image', async () => {
@@ -148,4 +150,29 @@ describe('document-capture/components/documents-step', () => {
expect(queryByRole('heading', { name: 'doc_auth.not_ready.header', level: 2 })).to.be.null();
});
});
+
+ context('selfie capture', () => {
+ it('renders with front, back, and selfie inputs when featureflag is on', () => {
+ const App = composeComponents(
+ [
+ FeatureFlagContext.Provider,
+ {
+ value: {
+ selfieCaptureEnabled: true,
+ },
+ },
+ ],
+ [DocumentsStep],
+ );
+ const { 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');
+
+ expect(front).to.be.ok();
+ expect(back).to.be.ok();
+ expect(selfie).to.be.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 aa44c2c17a6..e7eac8637d1 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
@@ -166,13 +166,37 @@ describe('document-capture/components/review-issues-step', () => {
expect(getByLabelText('doc_auth.headings.document_capture_front')).to.be.ok();
});
- it('renders with front and back inputs', async () => {
- const { getByLabelText, getByRole } = render();
+ it('renders with only front and back inputs only by default', async () => {
+ const { getByLabelText, queryByLabelText, getByRole } = render(
+ ,
+ );
+
+ await userEvent.click(getByRole('button', { name: 'idv.failure.button.warning' }));
+
+ expect(getByLabelText('doc_auth.headings.document_capture_front')).to.be.ok();
+ expect(getByLabelText('doc_auth.headings.document_capture_back')).to.be.ok();
+ expect(queryByLabelText('doc_auth.headings.document_capture_selfie')).to.not.exist();
+ });
+
+ it('renders with front, back, and selfie inputs when featureflag', async () => {
+ const App = composeComponents(
+ [
+ FeatureFlagContext.Provider,
+ {
+ value: {
+ selfieCaptureEnabled: true,
+ },
+ },
+ ],
+ [ReviewIssuesStep, DEFAULT_PROPS],
+ );
+ const { getByLabelText, queryByLabelText, getByRole } = render();
await userEvent.click(getByRole('button', { name: 'idv.failure.button.warning' }));
expect(getByLabelText('doc_auth.headings.document_capture_front')).to.be.ok();
expect(getByLabelText('doc_auth.headings.document_capture_back')).to.be.ok();
+ expect(queryByLabelText('doc_auth.headings.document_capture_selfie')).to.be.ok();
});
it('calls onChange callback with uploaded image', async () => {
diff --git a/spec/javascript/support/document-capture.jsx b/spec/javascript/support/document-capture.jsx
index bd480bd36f0..24dea01116a 100644
--- a/spec/javascript/support/document-capture.jsx
+++ b/spec/javascript/support/document-capture.jsx
@@ -60,6 +60,7 @@ export function useAcuant() {
delete window.AcuantJavascriptWebSdk;
delete window.AcuantCamera;
delete window.AcuantCameraUI;
+ delete window.AcuantPassiveLiveness;
delete window.loadAcuantSdk;
});
@@ -91,6 +92,7 @@ export function useAcuant() {
}),
end,
};
+ window.AcuantPassiveLiveness = { start: sinon.stub(), end: sinon.stub() };
window.loadAcuantSdk = () => {};
const sdkScript = document.querySelector('[data-acuant-sdk]');
sdkScript.onload();
diff --git a/spec/jobs/reports/monthly_key_metrics_report_spec.rb b/spec/jobs/reports/monthly_key_metrics_report_spec.rb
index a11f14a413a..c3ab446953e 100644
--- a/spec/jobs/reports/monthly_key_metrics_report_spec.rb
+++ b/spec/jobs/reports/monthly_key_metrics_report_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
RSpec.describe Reports::MonthlyKeyMetricsReport do
- let(:report_date) { Date.new(2021, 3, 2) }
+ let(:report_date) { Date.new(2021, 3, 2).in_time_zone('UTC').end_of_day }
subject(:report) { Reports::MonthlyKeyMetricsReport.new(report_date) }
let(:name) { 'monthly-key-metrics-report' }
diff --git a/spec/mailers/previews/report_mailer_preview.rb b/spec/mailers/previews/report_mailer_preview.rb
index da697115001..a4f691ceca0 100644
--- a/spec/mailers/previews/report_mailer_preview.rb
+++ b/spec/mailers/previews/report_mailer_preview.rb
@@ -15,7 +15,7 @@ def monthly_key_metrics_report
ReportMailer.tables_report(
email: 'test@example.com',
- subject: 'Example Key Metrics Report',
+ subject: "Example Key Metrics Report - #{Time.zone.now.to_date}",
message: monthly_key_metrics_report.preamble,
attachment_format: :xlsx,
reports: monthly_key_metrics_report.reports,
diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb
index 3bef56f1d54..7aedde0f5b3 100644
--- a/spec/mailers/previews/user_mailer_preview.rb
+++ b/spec/mailers/previews/user_mailer_preview.rb
@@ -59,6 +59,7 @@ def new_device_sign_in
UserMailer.with(user: user, email_address: email_address_record).new_device_sign_in(
date: 'February 25, 2019 15:02',
location: 'Washington, DC',
+ device_name: 'Chrome ABC on macOS 123',
disavowal_token: SecureRandom.hex,
)
end
diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb
index 62878274f3d..0e40835743b 100644
--- a/spec/mailers/user_mailer_spec.rb
+++ b/spec/mailers/user_mailer_spec.rb
@@ -177,11 +177,13 @@
describe '#new_device_sign_in' do
date = 'February 25, 2019 15:02'
location = 'Washington, DC'
+ device_name = 'Chrome ABC on macOS 123'
disavowal_token = 'asdf1234'
let(:mail) do
UserMailer.with(user: user, email_address: email_address).new_device_sign_in(
date: date,
location: location,
+ device_name: device_name,
disavowal_token: disavowal_token,
)
end
@@ -201,8 +203,11 @@
to have_content(
strip_tags(
t(
- 'user_mailer.new_device_sign_in.info_html',
- date: date, location: location, app_name: APP_NAME,
+ 'user_mailer.new_device_sign_in.info',
+ date: date,
+ location: location,
+ device_name: device_name,
+ app_name: APP_NAME,
),
),
)
@@ -217,6 +222,7 @@
mail = UserMailer.with(user: user, email_address: email_address).new_device_sign_in(
date: date,
location: location,
+ device_name: device_name,
disavowal_token: disavowal_token,
)
expect(mail.to).to eq(nil)
diff --git a/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb b/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb
index 4c66d801dae..31ce444326d 100644
--- a/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb
+++ b/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb
@@ -4,9 +4,9 @@
let(:user) { build(:user) }
let(:profile) { build(:profile, user: user) }
let(:current_address_matches_id) { true }
- let(:created_at) { described_class::USPS_SERVER_TIMEZONE.parse('2022-07-14T00:00:00Z') }
+ let(:created_at) { described_class::USPS_SERVER_TIMEZONE.parse('2023-06-14T00:00:00Z') }
let(:enrollment_established_at) do
- described_class::USPS_SERVER_TIMEZONE.parse('2022-08-14T00:00:00Z')
+ described_class::USPS_SERVER_TIMEZONE.parse('2023-07-14T00:00:00Z')
end
let(:enrollment_selected_location_details) do
JSON.parse(UspsInPersonProofing::Mock::Fixtures.enrollment_selected_location_details)
@@ -31,13 +31,13 @@
end
it 'returns a formatted due date' do
- expect(formatted_due_date).to eq 'September 12, 2022'
+ expect(formatted_due_date).to eq 'August 12, 2023'
end
context 'there is no enrollment_established_at' do
let(:enrollment_established_at) { nil }
it 'returns formatted due date when no enrollment_established_at' do
- expect(formatted_due_date).to eq 'August 12, 2022'
+ expect(formatted_due_date).to eq 'July 13, 2023'
end
end
end
diff --git a/spec/requests/saml_requests_spec.rb b/spec/requests/saml_requests_spec.rb
index a5748cda323..1603977bd4e 100644
--- a/spec/requests/saml_requests_spec.rb
+++ b/spec/requests/saml_requests_spec.rb
@@ -45,7 +45,7 @@
it 'does not set a session cookie' do
post saml_settings.idp_sso_target_url
- new_cookies = response.header['Set-Cookie'].split("\n").map do |c|
+ new_cookies = response.header['set-cookie'].map do |c|
cookie_regex.match(c)[:cookie]
end
diff --git a/spec/requests/secure_cookies_spec.rb b/spec/requests/secure_cookies_spec.rb
index 7a72fa05cea..aa4699fe565 100644
--- a/spec/requests/secure_cookies_spec.rb
+++ b/spec/requests/secure_cookies_spec.rb
@@ -4,28 +4,30 @@
context 'with plain HTTP' do
it 'flags all cookies sent by the application as HttpOnly and SameSite=Lax' do
get root_url
- cookie_count = response.headers['Set-Cookie'].split("\n").count
+ cookies = response.headers['Set-Cookie']
+ cookie_count = cookies.count
- expect(response.headers['Set-Cookie']).to_not include('; Secure')
- expect(response.headers['Set-Cookie'].scan('; HttpOnly').size).to eq(cookie_count)
- expect(response.headers['Set-Cookie'].scan('; SameSite=Lax').size).to eq(cookie_count)
+ expect(cookies.any? { |x| x.match?(SecureCookies::SECURE_REGEX) }).to eq(false)
+ expect(cookies.count { |x| x.match?(SecureCookies::HTTP_ONLY_REGEX) }).to eq(cookie_count)
+ expect(cookies.count { |x| x.match?(SecureCookies::SAME_SITE_REGEX) }).to eq(cookie_count)
end
end
context 'with HTTPS' do
it 'flags all cookies sent by the application as Secure, HttpOnly, and SameSite=Lax' do
get root_url, headers: { 'HTTPS' => 'on' }
- cookie_count = response.headers['Set-Cookie'].split("\n").count
+ cookie_count = response.headers['Set-Cookie'].count
+ cookies = response.headers['Set-Cookie']
- expect(response.headers['Set-Cookie'].scan('; Secure').size).to eq(cookie_count)
- expect(response.headers['Set-Cookie'].scan('; HttpOnly').size).to eq(cookie_count)
- expect(response.headers['Set-Cookie'].scan('; SameSite=Lax').size).to eq(cookie_count)
+ expect(cookies.count { |x| x.match?(SecureCookies::SECURE_REGEX) }).to eq(cookie_count)
+ expect(cookies.count { |x| x.match?(SecureCookies::HTTP_ONLY_REGEX) }).to eq(cookie_count)
+ expect(cookies.count { |x| x.match?(SecureCookies::SAME_SITE_REGEX) }).to eq(cookie_count)
end
end
it 'does not set an expiration on the session cookie' do
get root_url
- cookies = response.headers['Set-Cookie'].split("\n")
+ cookies = response.headers['Set-Cookie']
session_cookie = cookies.find { |x| x.include?(APPLICATION_SESSION_COOKIE_KEY) }
expect(session_cookie).to_not include('expires=')
end
diff --git a/spec/services/reporting/account_reuse_report_spec.rb b/spec/services/reporting/account_reuse_report_spec.rb
index a5e08e1b4b3..a88817acf2f 100644
--- a/spec/services/reporting/account_reuse_report_spec.rb
+++ b/spec/services/reporting/account_reuse_report_spec.rb
@@ -102,6 +102,7 @@ def create_identity(id, provider, verified_time)
]
aggregate_failures do
+ expect(report.account_reuse_emailable_report.title).to eq 'IDV app reuse rate Feb-2021'
report.account_reuse_emailable_report.table.zip(expected_csv).each do |actual, expected|
expect(actual).to eq(expected)
end
diff --git a/spec/support/features/webauthn_helper.rb b/spec/support/features/webauthn_helper.rb
index 963f6b2dda3..2800a436c60 100644
--- a/spec/support/features/webauthn_helper.rb
+++ b/spec/support/features/webauthn_helper.rb
@@ -127,6 +127,15 @@ def set_hidden_field(id, value)
end
end
+ def simulate_platform_authenticator_available
+ page.evaluate_script(<<~JS)
+ window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = () => Promise.resolve(true);
+ JS
+ page.evaluate_script(<<~JS)
+ document.querySelectorAll('lg-webauthn-input').forEach((input) => input.connectedCallback());
+ JS
+ end
+
def protocol
'http://'
end
diff --git a/spec/views/devise/sessions/new.html.erb_spec.rb b/spec/views/devise/sessions/new.html.erb_spec.rb
index 3fe8664165b..039e2a616a6 100644
--- a/spec/views/devise/sessions/new.html.erb_spec.rb
+++ b/spec/views/devise/sessions/new.html.erb_spec.rb
@@ -75,6 +75,15 @@
)
end
+ it 'includes tracking script for no-JavaScript' do
+ render
+
+ expect(rendered).to have_css(
+ "link[rel='stylesheet'][href='#{no_js_detect_css_path(location: :sign_in)}']",
+ visible: false,
+ )
+ end
+
context 'when SP is present' do
let(:sp) do
build_stubbed(