diff --git a/config/application.yml.default b/config/application.yml.default
index 413ca9371d7..139be4992b6 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -51,6 +51,7 @@ aws_logo_bucket: ''
aws_region: 'us-west-2'
aws_kms_multi_region_enabled: false
backup_code_cost: '2000$8$1$'
+backup_code_reminder_redirect: false
broken_personal_key_window_start: '2021-07-29T00:00:00Z'
broken_personal_key_window_finish: '2021-09-22T00:00:00Z'
country_phone_number_overrides: '{}'
@@ -100,6 +101,7 @@ idv_api_enabled_steps: '[]'
idv_attempt_window_in_hours: 6
idv_max_attempts: 5
idv_min_age_years: 13
+idv_personal_key_confirmation_enabled: true
idv_public_key: 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBS3p4d25rbUxqeGx1NmhsRlQ2d2JreUlweHNtYkMyaApjYW5TMGhuWm1DRGIrTEhaME5zQTdHWURpZkMxQlRBMHRuRFo0Zm9HNTRmYjNzYk9ubGpGWXVNQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo='
idv_private_key: 'LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT2dJQkFBSkJBS3p4d25rbUxqeGx1NmhsRlQ2d2JreUlweHNtYkMyaGNhblMwaG5abUNEYitMSFowTnNBCjdHWURpZkMxQlRBMHRuRFo0Zm9HNTRmYjNzYk9ubGpGWXVNQ0F3RUFBUUpCQUp6TUMvOSs2RWlHQzkrZTFlWWkKVzc0ejN4MjBkanZndFlhOHh4UDh2ZnA3TjdKQXMvaGNUbjVLOCtDM2swaXUyR2RNb21qSlp2ckxwT0IyTWh4RQo3QkVDSVFEVERhbVRCMHhKSlVpV0ljNk15Y0dFa2J4SEZ3eEtURVNCaHhzREFISDZEUUloQU5IR2NwVUs5dmVSCkdrZlZTOS9MSVNZQlk2YzRUZk1NUFJZU21KVHFNRVN2QWlBZFdiY05aV1JzZjZ6YWhCVVBhemRvVWtRV3R0UFUKdVVxRm9ONVd5b2NQT1FJZ1FrUjlaK1haMUtVcTl5eERWc1FWaWFzQXJ3K1RXRWN5ZU9tUTkrSHZNNU1DSUcrMQpxVldqNW9PL0FBSU1QbXZVZmp5L0JnMnhEQVRiOEp6alFrQ3dLSnNwCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg=='
idv_send_link_attempt_window_in_minutes: 10
@@ -112,7 +114,7 @@ in_person_results_delay_in_hours: 1
include_slo_in_saml_metadata: false
irs_attempt_api_audience: 'https://irs.gov'
irs_attempt_api_auth_tokens: ''
-irs_attempt_api_csp_id: 'Login.gov'
+irs_attempt_api_csp_id: 'LOGIN.gov'
irs_attempt_api_enabled: false
irs_attempt_api_event_ttl_seconds: 86400
irs_attempt_api_event_count_default: 1000
@@ -319,12 +321,13 @@ development:
hmac_fingerprinter_key: a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c
hmac_fingerprinter_key_queue: '["11111111111111111111111111111111", "22222222222222222222222222222222"]'
identity_pki_local_dev: true
- idv_api_enabled_steps: '["password_confirm", "personal_key","personal_key_confirm"]'
in_person_proofing_enabled: true
kantara_2fa_phone_restricted: true
kantara_2fa_phone_existing_user_restriction: true
kantara_restriction_enforcement_date: '2022-07-01'
irs_attempt_api_public_key: MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyut9Uio5XxsIUVrXARqCoHvcMVYT0p6WyU1BnbhxLRW4Q60p+4Bn32vVOt9nzeih7qvauYM5M0PZdKEmwOHflqPP+ABfKhL+6jxBhykN5P5UY375wTFBJZ20Fx8jOJbRhJD02oUQ49YKlDu3MG5Y0ApyD4ER4WKgxuB2OdyQKd9vg2ZZa+P2pw1HkFPEin0h8KBUFBeLGDZni8PIJdHBP6dA+xbayGBxSM/8xQC0JIg6KlGTcLql37QJIhP2oSv0nAJNb6idFPAz0uMCQDQWKKWV5FUDCsFVH7VuQz8xUCwnPn/SdaratB+29bwUpVhgHXrHdJ0i8vjBEX7smD7pI8CcFHuVgACt86NMlBnNCVkwumQgZNAAxe2mJoYcotEWOnhCuMc6MwSj985bj8XEdFlbf4ny9QO9rETd5aYcwXBiV/T6vd637uvHb0KenghNmlb1Tv9LMj2b9ZwNc9C6oeCnbN2YAfxSDrb8Ik+yq4hRewOvIK7f0CcpZYDXK25aHXnHm306Uu53KIwMGf1mha5T5LWTNaYy5XFoMWHJ9E+AnU/MUJSrwCAITH/S0JFcna5Oatn70aTE9pISATsqB5Iz1c46MvdrxD8hPoDjT7x6/EO316DZrxQfJhjbWsCB+R0QxYLkXPHczhB2Z0HPna9xB6RbJHzph7ifDizhZoMCAwEAAQ==
+ irs_attempt_api_enabled: true
+ irs_attempt_api_auth_tokens: 'abc123'
liveness_checking_enabled: true
logins_per_ip_limit: 5
logo_upload_enabled: true
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 711fe6db86a..0ae75ffc808 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -36,10 +36,6 @@
config.middleware.use RackSessionAccess::Middleware
- # Disable lograge when computing coverage and not in CircleCI, where lograge is required.
- # This enables scanning for view test coverage with `rake test:scan_log_for_view_coverage`
- config.lograge.enabled = !ENV['COVERAGE'] || ENV['CI']
-
config.after_initialize do
# Having bullet enabled in the test environment causes issues with unit
# tests that may not make user of eager loaded values. We disable it by
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index f60177a61a2..bdfd10bee85 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -33,7 +33,7 @@
if auth.env['action_dispatch.cookies']
expected_cookie_value = "#{user.class}-#{user.id}"
actual_cookie_value = auth.env['action_dispatch.cookies'].
- signed[TwoFactorAuthenticatable::REMEMBER_2FA_COOKIE]
+ signed[TwoFactorAuthenticatable::REMEMBER_2FA_COOKIE]
bypass_by_cookie = actual_cookie_value == expected_cookie_value
end
diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb
index 7990dcc33cd..52098335a39 100644
--- a/config/initializers/simple_form.rb
+++ b/config/initializers/simple_form.rb
@@ -1,6 +1,7 @@
# rubocop:disable Metrics/BlockLength
SimpleForm.setup do |config|
require Rails.root.join('lib', 'extensions', 'simple_form', 'error_notification')
+ require Rails.root.join('lib', 'extensions', 'simple_form', 'components', 'submit_component')
config.button_class = 'usa-button'
config.boolean_label_class = nil
diff --git a/config/locales/account/en.yml b/config/locales/account/en.yml
index eb707b2e7f1..f5f5c075df3 100644
--- a/config/locales/account/en.yml
+++ b/config/locales/account/en.yml
@@ -53,11 +53,9 @@ en:
totp_confirm_delete: Yes, remove authentication app
unknown_location: unknown location
verification:
- bounced: The postal service could not deliver the letter to your address.
instructions: Your account requires a confirmation code to be verified.
reactivate_button: Enter the code you received via US mail
success: Your account has been verified.
- update_address: Please update your address to be verified.
webauthn: Security key
webauthn_add: Add security key
webauthn_confirm_delete: Yes, remove key
diff --git a/config/locales/account/es.yml b/config/locales/account/es.yml
index ad4e43f3db8..30f02a3ca05 100644
--- a/config/locales/account/es.yml
+++ b/config/locales/account/es.yml
@@ -54,11 +54,9 @@ es:
totp_confirm_delete: Sí, eliminar la aplicación de autenticación
unknown_location: ubicación desconocida
verification:
- bounced: El servicio postal no pudo entregar la carta a su dirección.
instructions: Su cuenta requiere un código de confirmación para ser verificado.
reactivate_button: Ingrese el código que recibió por correo postal.
success: Su cuenta ha sido verificada.
- update_address: Actualice su dirección para ser verificada.
webauthn: Clave de seguridad
webauthn_add: Añadir clave de seguridad
webauthn_confirm_delete: Si quitar la llave
diff --git a/config/locales/account/fr.yml b/config/locales/account/fr.yml
index c438c393d60..bd0884fed08 100644
--- a/config/locales/account/fr.yml
+++ b/config/locales/account/fr.yml
@@ -57,11 +57,9 @@ fr:
totp_confirm_delete: Oui, supprimez l’application d’authentification
unknown_location: lieu inconnu
verification:
- bounced: Le service postal n’a pas pu envoyer la lettre à votre adresse.
instructions: Votre compte nécessite un code de confirmation pour être vérifié.
reactivate_button: Entrez le code que vous avez reçu par la poste
success: Votre compte a été vérifié.
- update_address: Veuillez mettre à jour votre adresse pour être vérifiée.
webauthn: Clé de sécurité
webauthn_add: Ajouter une clé de sécurité
webauthn_confirm_delete: Oui, supprimer la clé
diff --git a/config/locales/forms/en.yml b/config/locales/forms/en.yml
index 77c0c3db60e..4478278956a 100644
--- a/config/locales/forms/en.yml
+++ b/config/locales/forms/en.yml
@@ -27,6 +27,12 @@ en:
caution: If you regenerate your backup codes you will receive a new set of
backup codes. Your original backup codes will no longer be valid.
confirm: Are you sure you want to regenerate your backup codes?
+ backup_code_reminder:
+ body_info: If you ever lose access to your primary authentication method, you
+ can use backup codes to regain access to your account.
+ have_codes: I have my codes
+ heading: Do you still have your backup codes?
+ need_new_codes: I need a new set of backup codes
buttons:
back: Back
cancel: Yes, cancel
@@ -108,10 +114,15 @@ en:
validation:
required_checkbox: Please check this box to continue
verify_profile:
- instructions: Enter the ten-character code in the letter we sent you.
+ instructions: Enter the 10-character code from the letter you received.
name: Confirmation code
submit: Confirm account
title: Confirm your account
+ welcome_back: Welcome back
+ welcome_back_description: '
If you have received your letter, please enter
+ your confirmation code below.
If your letter hasn’t arrived
+ yet, please be patient as letters typically take 3 to 7 business
+ days to arrive. Thank you for your patience.
'
webauthn_delete:
caution: If you remove your security key you won’t be able to use it to access
your %{app_name} account.
diff --git a/config/locales/forms/es.yml b/config/locales/forms/es.yml
index e8792640ece..4d449c235a5 100644
--- a/config/locales/forms/es.yml
+++ b/config/locales/forms/es.yml
@@ -32,6 +32,12 @@ es:
no serán válidos.
confirm: '¿Está seguro de que desea volver a generar sus códigos de copia de
seguridad?'
+ backup_code_reminder:
+ body_info: Si por alguna razón no puede acceder a su método de autenticación
+ principal, puede usar códigos de recuperación para ingresar a su cuenta.
+ have_codes: Tengo mis códigos
+ heading: '¿Todavía tiene sus códigos de recuperación?'
+ need_new_codes: Necesito un nuevo conjunto de códigos de recuperación
buttons:
back: Atrás
cancel: Sí, cancelar
@@ -115,10 +121,15 @@ es:
validation:
required_checkbox: Marque esta casilla para continuar
verify_profile:
- instructions: Ingrese el código de 10 caracteres que le enviamos en la carta.
+ instructions: Introduzca el código de 10 caracteres de la carta que ha recibido.
name: Código de confirmación
submit: Confirmar cuenta
title: Confirme su cuenta
+ welcome_back: Bienvenido de nuevo
+ welcome_back_description: '
Si ha recibido su carta, introduzca su código de
+ confirmación a continuación.
Si su carta aún no ha llegado, tenga
+ paciencia, ya que las cartas suelen tardar de 3 a 7 días hábiles
+ en llegar. Gracias por su paciencia.
'
webauthn_delete:
caution: Si elimina su clave de seguridad, no podrá usarla para acceder a su
cuenta %{app_name}.
diff --git a/config/locales/forms/fr.yml b/config/locales/forms/fr.yml
index 84eb16b2492..97cb4bd26ff 100644
--- a/config/locales/forms/fr.yml
+++ b/config/locales/forms/fr.yml
@@ -32,6 +32,13 @@ fr:
ensemble de codes de sauvegarde. Vos codes de sauvegarde d’origine ne
seront plus valides.
confirm: Êtes-vous sûr de vouloir régénérer vos codes de sauvegarde?
+ backup_code_reminder:
+ body_info: Si vous perdez l’accès à votre méthode d’authentification principale,
+ vous pouvez utiliser des codes de sauvegarde pour accéder à nouveau à
+ votre compte.
+ have_codes: J’ai mes codes
+ heading: Avez-vous toujours vos codes de sauvegarde?
+ need_new_codes: J’ai besoin d’un nouvel ensemble de codes de sauvegarde
buttons:
back: Retour
cancel: Oui, annuler
@@ -115,11 +122,17 @@ fr:
validation:
required_checkbox: Veuillez cocher cette case pour continuer
verify_profile:
- instructions: Entrez le code à dix caractères qui se trouve dans la lettre que
- nous vous avons envoyée.
+ instructions: Entrez le code à 10 caractères figurant sur la lettre que vous
+ avez reçue.
name: Code de confirmation
submit: Confirmer le compte
title: Confirmez votre compte
+ welcome_back: Content de vous revoir
+ welcome_back_description: '
Si vous avez reçu votre lettre, veuillez entrer
+ votre code de confirmation ci-dessous.
Si votre lettre n’est pas
+ encore arrivée, veuillez être patient car les lettres prennent
+ généralement entre trois à sept jours ouvrables pour arriver.
+ Nous vous remercions de votre patience.
'
webauthn_delete:
caution: Si vous supprimez votre clé de sécurité, vous ne pourrez plus
l’utiliser pour accéder à votre compte %{app_name}.
diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml
index 395fd16ae26..13df5213250 100644
--- a/config/locales/idv/en.yml
+++ b/config/locales/idv/en.yml
@@ -85,8 +85,7 @@ en:
timeout: We are experiencing higher than usual wait time processing your
request. Please try again.
forgot_password:
- link_html: Forgot password? %{link}
- link_text: Follow these instructions
+ link_text: Forgot password?
modal_header: Are you sure you can’t remember your password?
reset_password: Reset password
try_again: Try again
@@ -124,13 +123,11 @@ en:
come_back_later_sp_html: You can return to %{sp} for now.
confirm: You have encrypted your verified data
gpo:
- address_on_file: We will mail a letter with a confirmation code to the address
- that you provided on the previous step.
- new_address: We will mail a letter with a confirmation code to the address you
- specify.
+ address_on_file_html: We will mail a letter with a confirmation
+ code to the address that you provided on the previous step.
resend: Send me another letter
- timeframe: Letters are sent the next business day via USPS First Class Mail and
- typically take 3 to 7 business days.
+ timeframe_html: Letters are sent the next business day via USPS First Class Mail
+ and typically take 3 to 7 business days to arrive.
mail_sent: Your letter is on its way
otp_delivery_method:
phone_number_html: We’ll send a code to %{phone} to verify that
diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml
index 74460ccaee2..cde86dbcfac 100644
--- a/config/locales/idv/es.yml
+++ b/config/locales/idv/es.yml
@@ -91,8 +91,7 @@ es:
timeout: Estamos experimentando un tiempo de espera superior al habitual al
procesar su solicitud. Inténtalo de nuevo.
forgot_password:
- link_html: '¿Se te olvidó tu contraseña? %{link}'
- link_text: Siga estas instrucciones
+ link_text: '¿Se te olvidó tu contraseña?'
modal_header: '¿Estás seguro de que no puedes recordar tu contraseña?'
reset_password: Restablecer la contraseña
try_again: Inténtalo de nuevo
@@ -129,13 +128,13 @@ es:
come_back_later_sp_html: Ahora puedes volver a %{sp}.
confirm: Usted ha encriptado sus datos verificados.
gpo:
- address_on_file: Le enviaremos una carta con un código de confirmación a la
- dirección que nos facilitó en el paso anterior.
- new_address: Le enviaremos una carta con un código de confirmación a la
- dirección que especifique.
+ address_on_file_html: Le enviaremos una carta con un código de
+ confirmación a la dirección que nos facilitó en el paso
+ anterior.
resend: Envíeme otra carta
- timeframe: Las correspondencias se envían al día siguiente por correo de primera
- clase de USPS y por lo general tardan entre 3 y 7 días laborables.
+ timeframe_html: Las cartas se envían al día siguiente por First Class Mail de
+ USPS y suelen tardar entre 3 y 7 días hábiles en
+ llegar.
mail_sent: Su carta está en camino
otp_delivery_method:
phone_number_html: Enviaremos un código a %{phone} para
diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml
index 1ab7443d94c..797b8094fee 100644
--- a/config/locales/idv/fr.yml
+++ b/config/locales/idv/fr.yml
@@ -95,8 +95,7 @@ fr:
timeout: Le temps d’attente pour le traitement de votre demande est plus long
que d’habitude Veuillez réessayer.
forgot_password:
- link_html: Mot de passe oublié? %{link}
- link_text: Suivez ces instructions
+ link_text: Mot de passe oublié?
modal_header: Êtes-vous sûr de ne pas pouvoir vous souvenir de votre mot de passe?
reset_password: Réinitialiser le mot de passe
try_again: Réessayer
@@ -136,14 +135,13 @@ fr:
come_back_later_sp_html: Vous pouvez revenir à %{sp} pour le moment.
confirm: Vous avez crypté vos données vérifiées
gpo:
- address_on_file: Nous enverrons une lettre avec un code de confirmation à
- l’adresse que vous avez indiquée à l’étape précédente.
- new_address: Nous vous enverrons une lettre avec un code de confirmation à
- l’adresse que vous spécifiez.
+ address_on_file_html: Nous enverrons une lettre avec un code de
+ confirmation à l’adresse que vous avez indiquée à l’étape
+ précédente.
resend: Envoyez-moi une autre lettre
- timeframe: Les lettres sont envoyées le jour ouvrable suivant par courrier de
- première classe USPS et prennent généralement de 3 à 7 jours
- ouvrables.
+ timeframe_html: Les lettres sont envoyées les jours ouvrables par courriel de
+ première classe de USPS et prennent généralement entre trois à
+ sept jours ouvrables pour être reçues.
mail_sent: Votre lettre est en route
otp_delivery_method:
phone_number_html: Nous enverrons un code à %{phone} pour
diff --git a/config/locales/notices/en.yml b/config/locales/notices/en.yml
index 7c8abe7065b..eb8f8ee4e82 100644
--- a/config/locales/notices/en.yml
+++ b/config/locales/notices/en.yml
@@ -2,6 +2,7 @@
en:
notices:
account_reactivation: Great! You have your personal key.
+ authenticated_successfully: Authenticated successfully.
backup_codes_configured: Backup codes were added to your account.
backup_codes_deleted: Your backup codes were deleted from your account.
dap_participation: We participate in the US government’s analytics program. See
diff --git a/config/locales/notices/es.yml b/config/locales/notices/es.yml
index ca61e20e78e..04550a8c2b3 100644
--- a/config/locales/notices/es.yml
+++ b/config/locales/notices/es.yml
@@ -2,6 +2,7 @@
es:
notices:
account_reactivation: '¡Estupendo! Tiene su clave personal.'
+ authenticated_successfully: Autenticado con éxito.
backup_codes_configured: Códigos de respaldo fueron agregados a tu cuenta.
backup_codes_deleted: Tus códigos de respaldo fueron eliminados de tu cuenta.
dap_participation: Participamos en el programa analítico del Gobierno de Estados
diff --git a/config/locales/notices/fr.yml b/config/locales/notices/fr.yml
index 4bcad803be3..30fa3da0fea 100644
--- a/config/locales/notices/fr.yml
+++ b/config/locales/notices/fr.yml
@@ -2,6 +2,7 @@
fr:
notices:
account_reactivation: Excellent! Vous avez votre clé personnelle.
+ authenticated_successfully: Authentifié avec succès.
backup_codes_configured: Les codes de sauvegarde ont été ajoutés à votre compte.
backup_codes_deleted: Vos codes de sauvegarde ont été supprimés de votre compte.
dap_participation: Nous participons au programme d’analytique du gouvernement
diff --git a/config/routes.rb b/config/routes.rb
index 5ddf2cace0d..a35df3bdf84 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -241,6 +241,7 @@
get '/users/two_factor_authentication' => 'users/two_factor_authentication#show',
as: :user_two_factor_authentication # route name is used by two_factor_authentication gem
get '/backup_code_refreshed' => 'users/backup_code_setup#refreshed'
+ get '/backup_code_reminder' => 'users/backup_code_setup#reminder'
get '/backup_code_setup' => 'users/backup_code_setup#index'
patch '/backup_code_setup' => 'users/backup_code_setup#create', as: :backup_code_create
patch '/backup_code_continue' => 'users/backup_code_setup#continue'
diff --git a/lib/asset_sources.rb b/lib/asset_sources.rb
index 067d81f748f..1e2579d4922 100644
--- a/lib/asset_sources.rb
+++ b/lib/asset_sources.rb
@@ -9,7 +9,7 @@ def get_sources(*names)
# See: app/javascript/packages/rails-i18n-webpack-plugin/extract-keys-webpack-plugin.js
regexp_locale_suffix = %r{\.(#{I18n.available_locales.join('|')})\.js$}
- load_manifest if !manifest || !cache_manifest
+ load_manifest_if_needed
locale_sources, sources = names.flat_map do |name|
manifest&.dig('entrypoints', name, 'assets', 'js')
@@ -22,13 +22,19 @@ def get_sources(*names)
end
def get_assets(*names)
- load_manifest if !manifest || !cache_manifest
+ load_manifest_if_needed
names.flat_map do |name|
manifest&.dig('entrypoints', name, 'assets')&.except('js')&.values&.flatten
end.uniq.compact
end
+ def get_integrity(path)
+ load_manifest_if_needed
+
+ manifest&.dig('integrity', path)
+ end
+
def load_manifest
self.manifest = begin
JSON.parse(File.read(manifest_path))
@@ -36,5 +42,11 @@ def load_manifest
nil
end
end
+
+ private
+
+ def load_manifest_if_needed
+ load_manifest if !manifest || !cache_manifest
+ end
end
end
diff --git a/lib/extensions/simple_form/components/submit_component.rb b/lib/extensions/simple_form/components/submit_component.rb
new file mode 100644
index 00000000000..2c69eb999d0
--- /dev/null
+++ b/lib/extensions/simple_form/components/submit_component.rb
@@ -0,0 +1,10 @@
+module SubmitComponent
+ def submit(*args, &block)
+ options = args.extract_options!
+ content = args.first
+ content = template.capture { yield(content) } if block
+ template.render SubmitButtonComponent.new(**options, &block).with_content(content)
+ end
+end
+
+SimpleForm::FormBuilder.send :include, SubmitComponent
diff --git a/lib/feature_management.rb b/lib/feature_management.rb
index 91b2dfd50c5..c572397d91b 100644
--- a/lib/feature_management.rb
+++ b/lib/feature_management.rb
@@ -116,6 +116,10 @@ def self.idv_api_enabled?
IdentityConfig.store.idv_api_enabled_steps.present?
end
+ def self.idv_personal_key_confirmation_enabled?
+ IdentityConfig.store.idv_personal_key_confirmation_enabled
+ end
+
# Manual allowlist for VOIPs, should only include known VOIPs that we use for smoke tests
# @return [Set] set of phone numbers normalized to e164
def self.voip_allowed_phones
diff --git a/lib/identity_config.rb b/lib/identity_config.rb
index 8055f92d158..10189484a0c 100644
--- a/lib/identity_config.rb
+++ b/lib/identity_config.rb
@@ -113,6 +113,7 @@ def self.build_store(config_map)
config.add(:aws_logo_bucket, type: :string)
config.add(:aws_region, type: :string)
config.add(:backup_code_cost, type: :string)
+ config.add(:backup_code_reminder_redirect, type: :boolean)
config.add(:broken_personal_key_window_start, type: :timestamp)
config.add(:broken_personal_key_window_finish, type: :timestamp)
config.add(:country_phone_number_overrides, type: :json)
@@ -179,6 +180,7 @@ def self.build_store(config_map)
config.add(:idv_attempt_window_in_hours, type: :integer)
config.add(:idv_max_attempts, type: :integer)
config.add(:idv_min_age_years, type: :integer)
+ config.add(:idv_personal_key_confirmation_enabled, type: :boolean)
config.add(:idv_private_key, type: :string)
config.add(:idv_public_key, type: :string)
config.add(:idv_send_link_attempt_window_in_minutes, type: :integer)
diff --git a/lib/tasks/attempts.rake b/lib/tasks/attempts.rake
index 93dcd1ca13d..3cc6571b324 100644
--- a/lib/tasks/attempts.rake
+++ b/lib/tasks/attempts.rake
@@ -11,17 +11,23 @@ namespace :attempts do
resp = conn.post('/api/irs_attempts_api/security_events', body) do |req|
req.headers['Authorization'] =
"Bearer #{IdentityConfig.store.irs_attempt_api_csp_id} #{auth_token}"
- end.body
-
- events = JSON.parse(resp)
+ end
+ encrypted_data = Base64.strict_decode64(resp.body)
+ iv = Base64.strict_decode64(resp.headers['x-payload-iv'])
+ encrypted_key = Base64.strict_decode64(resp.headers['x-payload-key'])
+ private_key = OpenSSL::PKey::RSA.new(File.read(private_key_path))
+ key = private_key.private_decrypt(encrypted_key)
+ decrypted = IrsAttemptsApi::EnvelopeEncryptor.decrypt(
+ encrypted_data: encrypted_data, key: key, iv: iv,
+ )
+ events = JSON.parse(decrypted)
if File.exist?(private_key_path)
- puts events['sets'].any? ? 'Decrypted events:' : 'No events returned.'
+ puts events.any? ? 'Decrypted events:' : 'No events returned.'
- key = OpenSSL::PKey::RSA.new(File.read(private_key_path))
- events['sets'].each do |_jti, event|
+ events.each do |_jti, event|
begin
- pp JSON.parse(JWE.decrypt(event, key))
+ pp JSON.parse(JWE.decrypt(event, private_key))
rescue
puts 'Failed to parse/decrypt event!'
end
diff --git a/lib/telephony/pinpoint/sms_sender.rb b/lib/telephony/pinpoint/sms_sender.rb
index 5b916710448..a4f77d31932 100644
--- a/lib/telephony/pinpoint/sms_sender.rb
+++ b/lib/telephony/pinpoint/sms_sender.rb
@@ -94,7 +94,7 @@ def phone_info(phone_number)
)
break if response
rescue Seahorse::Client::NetworkingError,
- Aws::Pinpoint::Errors::InternalServerErrorException => error
+ Aws::Pinpoint::Errors::ServiceError => error
PinpointHelper.notify_pinpoint_failover(
error: error,
region: sms_config.region,
diff --git a/spec/components/button_component_spec.rb b/spec/components/button_component_spec.rb
index f25760d3e64..21cd20d14e2 100644
--- a/spec/components/button_component_spec.rb
+++ b/spec/components/button_component_spec.rb
@@ -43,6 +43,30 @@
end
end
+ context 'as full width' do
+ let(:options) { { full_width: true } }
+
+ it 'renders with design system classes' do
+ expect(rendered).to have_css('button.usa-button.usa-button--full-width')
+ end
+ end
+
+ context 'as unstyled' do
+ let(:options) { { unstyled: true } }
+
+ it 'renders with design system classes' do
+ expect(rendered).to have_css('button.usa-button.usa-button--unstyled')
+ end
+ end
+
+ context 'as dangerous' do
+ let(:options) { { danger: true } }
+
+ it 'renders with design system classes' do
+ expect(rendered).to have_css('button.usa-button.usa-button--danger')
+ end
+ end
+
context 'with tag options' do
it 'renders as attributes' do
rendered = render_inline ButtonComponent.new(
diff --git a/spec/components/submit_button_component_spec.rb b/spec/components/submit_button_component_spec.rb
new file mode 100644
index 00000000000..19ebfc7e986
--- /dev/null
+++ b/spec/components/submit_button_component_spec.rb
@@ -0,0 +1,36 @@
+require 'rails_helper'
+
+RSpec.describe SubmitButtonComponent, type: :component do
+ let(:options) { {} }
+ let(:content) { 'Button' }
+
+ subject(:rendered) do
+ render_inline described_class.new(**options).with_content(content)
+ end
+
+ it 'renders the submit button custom element' do
+ expect(rendered).to have_css('lg-submit-button')
+ expect(rendered).to have_css('button.usa-button')
+ expect(rendered).to have_content(content)
+ end
+
+ it 'renders as big, wide by default' do
+ expect(rendered).to have_css('.usa-button.usa-button--big.usa-button--wide')
+ end
+
+ context 'with explicit big, wide options' do
+ let(:options) { { big: false, wide: false } }
+
+ it 'renders respecting big, wide options' do
+ expect(rendered).to have_css('.usa-button:not(.usa-button--big):not(.usa-button--wide)')
+ end
+ end
+
+ context 'with additional options' do
+ let(:options) { { unstyled: true, data: { foo: 'bar' } } }
+
+ it 'passes additional options through to ButtonComponent' do
+ expect(rendered).to have_css('.usa-button.usa-button--unstyled[data-foo="bar"]')
+ end
+ end
+end
diff --git a/spec/components/validated_field_component_spec.rb b/spec/components/validated_field_component_spec.rb
index 7d9e890620d..3ad4581a286 100644
--- a/spec/components/validated_field_component_spec.rb
+++ b/spec/components/validated_field_component_spec.rb
@@ -28,7 +28,10 @@
it 'renders aria-describedby to establish connection between input and error message' do
field = rendered.at_css('input')
- expect(field.attr('aria-describedby')).to start_with('validated-field-error-')
+ expect(field.attr('aria-describedby').split(' ')).to include(
+ start_with('validated-field-hint-'),
+ start_with('validated-field-error-'),
+ )
end
describe 'error message strings' do
@@ -73,7 +76,8 @@
it 'merges aria-describedby with the one applied by the field' do
field = rendered.at_css('input')
- expect(field.attr('aria-describedby')).to start_with('foo validated-field-error-')
+ expect(field.attr('aria-describedby')).to include('validated-field-error-')
+ expect(field.attr('aria-describedby')).to include('foo')
end
end
end
diff --git a/spec/controllers/account_reset/delete_account_controller_spec.rb b/spec/controllers/account_reset/delete_account_controller_spec.rb
index d69e47237b1..4afa58afa03 100644
--- a/spec/controllers/account_reset/delete_account_controller_spec.rb
+++ b/spec/controllers/account_reset/delete_account_controller_spec.rb
@@ -6,12 +6,12 @@
describe '#delete' do
it 'logs a good token to the analytics' do
user = create(:user, :signed_up, :with_backup_code)
- create(:phone_configuration, user: user, phone: '+1 703-555-1214')
+ create(:phone_configuration, user: user, phone: Faker::PhoneNumber.cell_phone)
create_list(:webauthn_configuration, 2, user: user)
create_account_reset_request_for(user)
grant_request(user)
- session[:granted_token] = AccountResetRequest.all[0].granted_token
+ session[:granted_token] = AccountResetRequest.first.granted_token
stub_analytics
properties = {
user_id: user.uuid,
@@ -29,6 +29,26 @@
expect(response).to redirect_to account_reset_confirm_delete_account_url
end
+ it 'logs a good token to the attempts api' do
+ user = create(:user, :signed_up, :with_backup_code)
+ create(:phone_configuration, user: user, phone: Faker::PhoneNumber.cell_phone)
+ create_list(:webauthn_configuration, 2, user: user)
+ create_account_reset_request_for(user)
+ grant_request(user)
+
+ session[:granted_token] = AccountResetRequest.first.granted_token
+ stub_attempts_tracker
+
+ expect(@irs_attempts_api_tracker).to receive(:account_reset_account_deleted).with(
+ success: true,
+ failure_reason: {},
+ )
+
+ delete :delete
+
+ expect(response).to redirect_to account_reset_confirm_delete_account_url
+ end
+
it 'redirects to root if the token does not match one in the DB' do
session[:granted_token] = 'foo'
stub_analytics
@@ -53,6 +73,23 @@
)
end
+ it 'logs an error in irs attempts tracker' do
+ session[:granted_token] = 'foo'
+ stub_attempts_tracker
+ properties = {
+ success: false,
+ failure_reason: { token: [t(
+ 'errors.account_reset.granted_token_invalid',
+ app_name: APP_NAME,
+ )] },
+ }
+ expect(@irs_attempts_api_tracker).to receive(:account_reset_account_deleted).with(
+ properties,
+ )
+
+ delete :delete
+ end
+
it 'displays a flash and redirects to root if the token is missing' do
stub_analytics
properties = {
@@ -95,7 +132,7 @@
expect(@analytics).to receive(:track_event).with('Account Reset: delete', properties)
travel_to(Time.zone.now + 2.days) do
- session[:granted_token] = AccountResetRequest.all[0].granted_token
+ session[:granted_token] = AccountResetRequest.first.granted_token
delete :delete
end
@@ -146,7 +183,7 @@
with('Account Reset: granted token validation', properties)
travel_to(Time.zone.now + 2.days) do
- get :show, params: { token: AccountResetRequest.all[0].granted_token }
+ get :show, params: { token: AccountResetRequest.first.granted_token }
end
expect(response).to redirect_to(root_url)
diff --git a/spec/controllers/account_reset/pending_controller_spec.rb b/spec/controllers/account_reset/pending_controller_spec.rb
index 63e97171a32..d296969afb7 100644
--- a/spec/controllers/account_reset/pending_controller_spec.rb
+++ b/spec/controllers/account_reset/pending_controller_spec.rb
@@ -26,6 +26,19 @@
expect(account_reset_request.reload.cancelled_at).to_not be_nil
end
+ it 'logs the cancellation in attempts api' do
+ stub_attempts_tracker
+
+ account_reset_request = AccountResetRequest.create(user: user, requested_at: 1.hour.ago)
+
+ expect(@irs_attempts_api_tracker).to receive(:track_event).
+ with(:account_reset_cancel_request, success: true)
+
+ post :cancel
+
+ expect(account_reset_request.reload.cancelled_at).to_not be_nil
+ end
+
context 'when the account reset request does not exist' do
it 'renders a 404' do
post :cancel
diff --git a/spec/controllers/account_reset/request_controller_spec.rb b/spec/controllers/account_reset/request_controller_spec.rb
index cf4cd94dd66..cc43803f51a 100644
--- a/spec/controllers/account_reset/request_controller_spec.rb
+++ b/spec/controllers/account_reset/request_controller_spec.rb
@@ -90,6 +90,17 @@
post :create
end
+ it 'logs the visit to attempts api' do
+ user = create(:user, :with_piv_or_cac, :with_backup_code)
+ stub_sign_in_before_2fa(user)
+ stub_attempts_tracker
+
+ expect(@irs_attempts_api_tracker).to receive(:track_event).
+ with(:account_reset_request_submitted, success: true)
+
+ get :create
+ end
+
it 'redirects to root if user not signed in' do
post :create
diff --git a/spec/controllers/api/irs_attempts_api_controller_spec.rb b/spec/controllers/api/irs_attempts_api_controller_spec.rb
index a5e0f9205b4..ac512f6198b 100644
--- a/spec/controllers/api/irs_attempts_api_controller_spec.rb
+++ b/spec/controllers/api/irs_attempts_api_controller_spec.rb
@@ -85,20 +85,25 @@
post :create, params: { timestamp: time.iso8601 }
expect(response.status).to eq(401)
+
+ request.headers['Authorization'] = nil
+
+ post :create, params: { timestamp: time.iso8601 }
+
+ expect(response.status).to eq(401)
end
- it 'renders new events' do
+ it 'renders encrypted events' do
post :create, params: { timestamp: time.iso8601 }
expect(response).to be_ok
+ expect(Base64.strict_decode64(response.headers['X-Payload-IV'])).to be_present
+ expect(Base64.strict_decode64(response.headers['X-Payload-Key'])).to be_present
+ expect(Base64.strict_decode64(response.body)).to be_present
- expected_response = {
- 'sets' => existing_events.to_h,
- }
- expect(JSON.parse(response.body)).to eq(expected_response)
expect(@analytics).to have_logged_event(
'IRS Attempt API: Events submitted',
- rendered_event_count: 3,
+ rendered_event_count: existing_events.count,
success: true,
timestamp: time.iso8601,
)
diff --git a/spec/controllers/concerns/idv/threat_metrix_concern_spec.rb b/spec/controllers/concerns/idv/threat_metrix_concern_spec.rb
index 6386089ccbc..a6ea567e454 100644
--- a/spec/controllers/concerns/idv/threat_metrix_concern_spec.rb
+++ b/spec/controllers/concerns/idv/threat_metrix_concern_spec.rb
@@ -16,7 +16,7 @@ def index; end
before do
allow(IdentityConfig.store).to receive(:proofing_device_profiling_collecting_enabled).
- and_return(ff_enabled)
+ and_return(ff_enabled)
end
context 'ff is set' do
diff --git a/spec/controllers/frontend_log_controller_spec.rb b/spec/controllers/frontend_log_controller_spec.rb
index 56c10df36b1..42e74701541 100644
--- a/spec/controllers/frontend_log_controller_spec.rb
+++ b/spec/controllers/frontend_log_controller_spec.rb
@@ -7,8 +7,8 @@
let(:fake_analytics) { FakeAnalytics.new }
let(:user) { create(:user, :with_phone, with: { phone: '+1 (202) 555-1212' }) }
let(:event) { 'Custom Event' }
- let(:payload) { { message: 'To be logged...' } }
- let(:params) { { event: event, payload: payload } }
+ let(:payload) { { 'message' => 'To be logged...' } }
+ let(:params) { { 'event' => event, 'payload' => payload } }
let(:json) { JSON.parse(response.body, symbolize_names: true) }
context 'user is signed in' do
@@ -92,7 +92,7 @@
it 'rejects a request without specifying event' do
expect(fake_analytics).not_to receive(:track_event)
- params.delete(:event)
+ params.delete('event')
action
expect(response).to have_http_status(:bad_request)
@@ -102,13 +102,34 @@
it 'rejects a request without specifying payload' do
expect(fake_analytics).not_to receive(:track_event)
- params.delete(:payload)
+ params.delete('payload')
action
expect(response).to have_http_status(:bad_request)
expect(json[:success]).to eq(false)
end
end
+
+ context 'for a named analytics method' do
+ let(:payload) { { 'field' => 'front', 'failed_attempts' => 0 } }
+ let(:params) do
+ {
+ 'event' => 'IdV: Native camera forced after failed attempts',
+ 'payload' => payload,
+ }
+ end
+
+ it 'logs the analytics event without the prefix' do
+ expect(fake_analytics).to receive(:track_event).with(
+ 'IdV: Native camera forced after failed attempts', payload
+ )
+
+ action
+
+ expect(response).to have_http_status(:ok)
+ expect(json[:success]).to eq(true)
+ end
+ end
end
context 'user is not signed in' do
diff --git a/spec/controllers/idv/gpo_controller_spec.rb b/spec/controllers/idv/gpo_controller_spec.rb
index bd75da3c548..8e15a7240cc 100644
--- a/spec/controllers/idv/gpo_controller_spec.rb
+++ b/spec/controllers/idv/gpo_controller_spec.rb
@@ -51,28 +51,6 @@
expect(response).to be_ok
end
- it 'renders wait page while job is in progress' do
- allow(controller).to receive(:async_state).and_return(
- ProofingSessionAsyncResult.new(
- status: ProofingSessionAsyncResult::IN_PROGRESS,
- ),
- )
- get :index
-
- expect(response).to render_template :wait
- end
-
- it 'logs an event when there is a timeout' do
- allow(controller).to receive(:async_state).and_return(
- ProofingSessionAsyncResult.new(
- status: ProofingSessionAsyncResult::MISSING,
- ),
- )
-
- get :index
- expect(@analytics).to have_logged_event('Proofing Address Result Missing', {})
- end
-
context 'with letter already sent' do
before do
allow_any_instance_of(Idv::GpoPresenter).to receive(:letter_already_sent?).and_return(true)
diff --git a/spec/controllers/sign_up/passwords_controller_spec.rb b/spec/controllers/sign_up/passwords_controller_spec.rb
index 2d552fcc77b..4fa5333069e 100644
--- a/spec/controllers/sign_up/passwords_controller_spec.rb
+++ b/spec/controllers/sign_up/passwords_controller_spec.rb
@@ -7,6 +7,9 @@
user = create(:user, :unconfirmed, confirmation_token: token)
stub_analytics
+ stub_attempts_tracker
+
+ allow(@irs_attempts_api_tracker).to receive(:track_event)
analytics_hash = {
success: true,
@@ -20,12 +23,6 @@
'User Registration: Email Confirmation',
{ errors: {}, error_details: nil, success: true, user_id: user.uuid },
)
- expect_any_instance_of(IrsAttemptsApi::Tracker).to receive(:track_event).with(
- :user_registration_email_confirmation,
- email: user.email_addresses.first.email,
- success: true,
- failure_reason: nil,
- )
expect(@analytics).to receive(:track_event).
with('Password Creation', analytics_hash)
@@ -35,6 +32,18 @@
}
user.reload
+
+ expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
+ :user_registration_password_submitted,
+ success: true,
+ failure_reason: {},
+ )
+ expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
+ :user_registration_email_confirmation,
+ email: user.email_addresses.first.email,
+ success: true,
+ failure_reason: nil,
+ )
expect(user.valid_password?('NewVal!dPassw0rd')).to eq true
expect(user.confirmed?).to eq true
end
@@ -70,6 +79,9 @@
user = create(:user, :unconfirmed, confirmation_token: token)
stub_analytics
+ stub_attempts_tracker
+
+ allow(@irs_attempts_api_tracker).to receive(:track_event)
analytics_hash = {
success: false,
@@ -89,18 +101,22 @@
'User Registration: Email Confirmation',
{ errors: {}, error_details: nil, success: true, user_id: user.uuid },
)
+ expect(@analytics).to receive(:track_event).
+ with('Password Creation', analytics_hash)
- expect_any_instance_of(IrsAttemptsApi::Tracker).to receive(:track_event).with(
+ post :create, params: { password_form: { password: 'NewVal' }, confirmation_token: token }
+
+ expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
+ :user_registration_password_submitted,
+ success: false,
+ failure_reason: { password: ['This password is too short (minimum is 12 characters)'] },
+ )
+ expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
:user_registration_email_confirmation,
email: user.email_addresses.first.email,
success: true,
failure_reason: nil,
)
-
- expect(@analytics).to receive(:track_event).
- with('Password Creation', analytics_hash)
-
- post :create, params: { password_form: { password: 'NewVal' }, confirmation_token: token }
end
end
diff --git a/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb
index f49749aa63f..673f1867c2c 100644
--- a/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb
@@ -38,7 +38,7 @@
with(analytics_hash)
expect(@irs_attempts_api_tracker).to receive(:track_event).
- with(:mfa_verify_backup_code, success: true)
+ with(:mfa_login_backup_code, success: true)
post :create, params: payload
end
@@ -64,7 +64,7 @@
with(analytics_hash)
expect(@irs_attempts_api_tracker).to receive(:track_event).
- with(:mfa_verify_backup_code, success: true)
+ with(:mfa_login_backup_code, success: true)
expect(@analytics).to receive(:track_event).
with('User marked authenticated', authentication_type: :valid_2fa)
@@ -91,7 +91,7 @@
it 'renders the show page' do
stub_attempts_tracker
expect(@irs_attempts_api_tracker).to receive(:track_event).
- with(:mfa_verify_backup_code, success: false)
+ with(:mfa_login_backup_code, success: false)
post :create, params: payload
expect(response).to render_template(:show)
expect(flash[:error]).to eq t('two_factor_authentication.invalid_backup_code')
@@ -115,7 +115,7 @@
it 're-renders the backup code entry screen' do
stub_attempts_tracker
expect(@irs_attempts_api_tracker).to receive(:track_event).
- with(:mfa_verify_backup_code, success: false)
+ with(:mfa_login_backup_code, success: false)
post :create, params: payload
expect(response).to render_template(:show)
@@ -137,10 +137,14 @@
with(properties)
expect(@irs_attempts_api_tracker).to receive(:track_event).
- with(:mfa_verify_backup_code, success: false)
+ with(:mfa_login_backup_code, success: false)
expect(@analytics).to receive(:track_event).
- with('Multi-Factor Authentication: max attempts reached')
+ with('Multi-Factor Authentication: max attempts reached')
+
+ expect(@irs_attempts_api_tracker).to receive(:mfa_login_rate_limited).
+ with(type: 'backup_code')
+
expect(PushNotification::HttpPush).to receive(:deliver).
with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user))
diff --git a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb
index dd0fcf9b9c4..7591213069b 100644
--- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb
@@ -114,7 +114,7 @@
expect(@analytics).to receive(:track_mfa_submit_event).
with(properties)
- expect(@irs_attempts_api_tracker).to receive(:mfa_verify_phone_otp_submitted).
+ expect(@irs_attempts_api_tracker).to receive(:mfa_login_phone_otp_submitted).
with({ reauthentication: false, success: false })
post :create, params:
@@ -172,13 +172,16 @@
with(properties)
expect(@analytics).to receive(:track_event).
- with('Multi-Factor Authentication: max attempts reached')
+ with('Multi-Factor Authentication: max attempts reached')
expect(PushNotification::HttpPush).to receive(:deliver).
with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user))
- expect(@irs_attempts_api_tracker).to receive(:mfa_verify_phone_otp_submitted).
+ expect(@irs_attempts_api_tracker).to receive(:mfa_login_phone_otp_submitted).
with({ reauthentication: false, success: false })
+ expect(@irs_attempts_api_tracker).to receive(:mfa_login_rate_limited).
+ with(type: 'otp')
+
post :create, params:
{ code: '12345',
otp_delivery_preference: 'sms' }
@@ -234,8 +237,22 @@
expect(@analytics).to receive(:track_event).
with('User marked authenticated', authentication_type: :valid_2fa)
- expect(@irs_attempts_api_tracker).to receive(:mfa_verify_phone_otp_submitted).
- with({ reauthentication: false, success: true })
+ expect(@irs_attempts_api_tracker).to receive(:mfa_login_phone_otp_submitted).
+ with(reauthentication: false, success: true)
+
+ post :create, params: {
+ code: subject.current_user.reload.direct_otp,
+ otp_delivery_preference: 'sms',
+ }
+ end
+
+ it 'tracks the attempt event with reauthentication parameter true' do
+ stub_attempts_tracker
+
+ subject.user_session[:context] = 'reauthentication'
+
+ expect(@irs_attempts_api_tracker).to receive(:mfa_login_phone_otp_submitted).
+ with(reauthentication: true, success: true)
post :create, params: {
code: subject.current_user.reload.direct_otp,
@@ -291,7 +308,7 @@
before do
stub_attempts_tracker
- expect(@irs_attempts_api_tracker).to receive(:mfa_verify_phone_otp_submitted).
+ expect(@irs_attempts_api_tracker).to receive(:mfa_login_phone_otp_submitted).
with({ reauthentication: false, success: false })
post :create, params: { code: '12345', otp_delivery_preference: 'sms' }
end
@@ -309,7 +326,7 @@
before do
stub_attempts_tracker
- expect(@irs_attempts_api_tracker).to receive(:mfa_verify_phone_otp_submitted).
+ expect(@irs_attempts_api_tracker).to receive(:mfa_login_phone_otp_submitted).
with({ reauthentication: false, success: true })
post :create, params: {
code: subject.current_user.direct_otp,
@@ -380,7 +397,7 @@
controller.user_session[:phone_id] = phone_id
expect(@irs_attempts_api_tracker).to receive(:mfa_enroll_phone_otp_submitted).
- with({ success: true })
+ with(success: true)
post(
:create,
@@ -410,7 +427,7 @@
context 'user enters an invalid code' do
before do
expect(@irs_attempts_api_tracker).to receive(:mfa_enroll_phone_otp_submitted).
- with({ success: false })
+ with(success: false)
post(
:create,
@@ -461,6 +478,19 @@
expect(@analytics).to have_received(:track_event).
with('Multi-Factor Authentication Setup', properties)
end
+
+ context 'user has exceeded the maximum number of attempts' do
+ it 'tracks the attempt event' do
+ allow_any_instance_of(User).to receive(:max_login_attempts?).and_return(true)
+ sign_in_before_2fa
+
+ stub_attempts_tracker
+ expect(@irs_attempts_api_tracker).to receive(:mfa_enroll_rate_limited).
+ with(type: 'otp')
+
+ post :create, params: { code: '12345', otp_delivery_preference: 'sms' }
+ end
+ end
end
context 'user does not include a code parameter' do
diff --git a/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb
index 5ec4561b2f3..8e81fa14877 100644
--- a/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb
@@ -152,11 +152,16 @@
}
stub_analytics
+ stub_attempts_tracker
expect(@analytics).to receive(:track_mfa_submit_event).
with(properties)
expect(@analytics).to receive(:track_event).
- with('Multi-Factor Authentication: max attempts reached')
+ with('Multi-Factor Authentication: max attempts reached')
+
+ expect(@irs_attempts_api_tracker).to receive(:mfa_login_rate_limited).
+ with(type: 'personal_key')
+
expect(PushNotification::HttpPush).to receive(:deliver).
with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user))
diff --git a/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb
index 62506060e7b..a0cb3ccbf6a 100644
--- a/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb
@@ -192,6 +192,9 @@
expect(@analytics).to receive(:track_event).
with('Multi-Factor Authentication: enter PIV CAC visited', attributes)
+ expect(@irs_attempts_api_tracker).to receive(:mfa_login_rate_limited).
+ with(type: 'piv_cac')
+
submit_attributes = {
success: false,
errors: { type: 'user.piv_cac_mismatch' },
@@ -211,7 +214,7 @@
)
expect(@analytics).to receive(:track_event).
- with('Multi-Factor Authentication: max attempts reached')
+ with('Multi-Factor Authentication: max attempts reached')
expect(PushNotification::HttpPush).to receive(:deliver).
with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user))
diff --git a/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb
index ececb339549..4765db3cde2 100644
--- a/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb
@@ -48,7 +48,7 @@
expect(@analytics).to receive(:track_event).
with('User marked authenticated', authentication_type: :valid_2fa)
expect(@irs_attempts_api_tracker).to receive(:track_event).
- with(:mfa_verify_totp, success: true)
+ with(:mfa_login_totp, success: true)
post :create, params: { code: generate_totp_code(@secret) }
end
@@ -94,9 +94,13 @@
expect(@analytics).to receive(:track_mfa_submit_event).
with(attributes)
expect(@analytics).to receive(:track_event).
- with('Multi-Factor Authentication: max attempts reached')
+ with('Multi-Factor Authentication: max attempts reached')
expect(@irs_attempts_api_tracker).to receive(:track_event).
- with(:mfa_verify_totp, success: false)
+ with(:mfa_login_totp, success: false)
+
+ expect(@irs_attempts_api_tracker).to receive(:mfa_login_rate_limited).
+ with(type: 'totp')
+
expect(PushNotification::HttpPush).to receive(:deliver).
with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user))
diff --git a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb
index 66ca69b9fed..abf127d8994 100644
--- a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb
@@ -83,7 +83,7 @@
with('User marked authenticated', authentication_type: :valid_2fa)
expect(@irs_attempts_api_tracker).to receive(:track_event).with(
- :mfa_verify_webauthn_roaming,
+ :mfa_login_webauthn_roaming,
success: true,
)
@@ -108,7 +108,7 @@
with('User marked authenticated', authentication_type: :valid_2fa)
expect(@irs_attempts_api_tracker).to receive(:track_event).with(
- :mfa_verify_webauthn_platform,
+ :mfa_login_webauthn_platform,
success: true,
)
@@ -151,8 +151,8 @@
let(:view_context) { ActionController::Base.new.view_context }
before do
allow_any_instance_of(TwoFactorAuthCode::WebauthnAuthenticationPresenter).
- to receive(:multiple_factors_enabled?).
- and_return(true)
+ to receive(:multiple_factors_enabled?).
+ and_return(true)
end
it 'redirects to webauthn show page' do
@@ -175,8 +175,8 @@
context 'User only has webauthn as an MFA method' do
before do
allow_any_instance_of(TwoFactorAuthCode::WebauthnAuthenticationPresenter).
- to receive(:multiple_factors_enabled?).
- and_return(false)
+ to receive(:multiple_factors_enabled?).
+ and_return(false)
end
it 'redirects to webauthn error page ' do
diff --git a/spec/controllers/users/additional_mfa_required_controller_spec.rb b/spec/controllers/users/additional_mfa_required_controller_spec.rb
index a7d0ee4d59d..0da826ce04d 100644
--- a/spec/controllers/users/additional_mfa_required_controller_spec.rb
+++ b/spec/controllers/users/additional_mfa_required_controller_spec.rb
@@ -28,7 +28,7 @@
let(:enforcement_date) { Time.zone.today + 6.days }
before do
allow(IdentityConfig.store).to receive(:kantara_restriction_enforcement_date).
- and_return(enforcement_date)
+ and_return(enforcement_date)
end
context 'before enforcement date' do
@@ -55,7 +55,7 @@
user.reload
expect(user.non_restricted_mfa_required_prompt_skip_date).
- to eq Time.zone.today
+ to eq Time.zone.today
end
it 'does not allow unauthenticated users' do
diff --git a/spec/controllers/users/email_confirmations_controller_spec.rb b/spec/controllers/users/email_confirmations_controller_spec.rb
index 8ae61c069e4..35abc25a95f 100644
--- a/spec/controllers/users/email_confirmations_controller_spec.rb
+++ b/spec/controllers/users/email_confirmations_controller_spec.rb
@@ -14,9 +14,9 @@
)).ordered
expect(PushNotification::HttpPush).to receive(:deliver).once.
- with(PushNotification::RecoveryInformationChangedEvent.new(
- user: user,
- )).ordered
+ with(PushNotification::RecoveryInformationChangedEvent.new(
+ user: user,
+ )).ordered
add_email_form = AddUserEmailForm.new
add_email_form.submit(user, email: new_email)
diff --git a/spec/controllers/users/reset_passwords_controller_spec.rb b/spec/controllers/users/reset_passwords_controller_spec.rb
index 467d5e4ae30..c956301c219 100644
--- a/spec/controllers/users/reset_passwords_controller_spec.rb
+++ b/spec/controllers/users/reset_passwords_controller_spec.rb
@@ -8,7 +8,9 @@
context 'no user matches token' do
it 'redirects to page where user enters email for password reset token' do
stub_analytics
+ stub_attempts_tracker
allow(@analytics).to receive(:track_event)
+ allow(@irs_attempts_api_tracker).to receive(:track_event)
get :edit, params: { reset_password_token: 'foo' }
@@ -21,6 +23,11 @@
expect(@analytics).to have_received(:track_event).
with('Password Reset: Token Submitted', analytics_hash)
+ expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
+ :forgot_password_email_confirmed,
+ success: false,
+ failure_reason: { user: ['invalid_token'] },
+ )
expect(response).to redirect_to new_user_password_path
expect(flash[:error]).to eq t('devise.passwords.invalid_token')
@@ -30,7 +37,9 @@
context 'token expired' do
it 'redirects to page where user enters email for password reset token' do
stub_analytics
+ stub_attempts_tracker
allow(@analytics).to receive(:track_event)
+ allow(@irs_attempts_api_tracker).to receive(:track_event)
user = instance_double('User', uuid: '123')
allow(User).to receive(:with_reset_password_token).with('foo').and_return(user)
@@ -47,7 +56,11 @@
expect(@analytics).to have_received(:track_event).
with('Password Reset: Token Submitted', analytics_hash)
-
+ expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
+ :forgot_password_email_confirmed,
+ success: false,
+ failure_reason: { user: ['token_expired'] },
+ )
expect(response).to redirect_to new_user_password_path
expect(flash[:error]).to eq t('devise.passwords.token_expired')
end
@@ -58,6 +71,8 @@
it 'displays the form to enter a new password and disallows indexing' do
stub_analytics
+ stub_attempts_tracker
+ allow(@irs_attempts_api_tracker).to receive(:track_event)
user = instance_double('User', uuid: '123')
email_address = instance_double('EmailAddress')
@@ -75,15 +90,29 @@
expect(response).to render_template :edit
expect(flash.keys).to be_empty
expect(response.body).to match('')
+ expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
+ :forgot_password_email_confirmed,
+ success: true,
+ failure_reason: {},
+ )
end
end
end
describe '#update' do
context 'user submits new password after token expires' do
+ let(:irs_tracker_failure_reason) do
+ {
+ password: [password_error_message],
+ reset_password_token: ['token_expired'],
+ }
+ end
+
it 'redirects to page where user enters email for password reset token' do
stub_analytics
+ stub_attempts_tracker
allow(@analytics).to receive(:track_event)
+ allow(@irs_attempts_api_tracker).to receive(:track_event)
raw_reset_token, db_confirmation_token =
Devise.token_generator.generate(User, :reset_password_token)
@@ -116,14 +145,26 @@
expect(@analytics).to have_received(:track_event).
with('Password Reset: Password Submitted', analytics_hash)
+ expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
+ :forgot_password_new_password_submitted,
+ success: false,
+ failure_reason: irs_tracker_failure_reason,
+ )
+
expect(response).to redirect_to new_user_password_path
expect(flash[:error]).to eq t('devise.passwords.token_expired')
end
end
context 'user submits invalid new password' do
+ let(:irs_tracker_failure_reason) do
+ { password: [password_error_message] }
+ end
+
it 'renders edit' do
stub_analytics
+ stub_attempts_tracker
+ allow(@irs_attempts_api_tracker).to receive(:track_event)
raw_reset_token, db_confirmation_token =
Devise.token_generator.generate(User, :reset_password_token)
@@ -153,6 +194,11 @@
expect(assigns(:forbidden_passwords)).to all(be_a(String))
expect(response).to render_template(:edit)
+ expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
+ :forgot_password_new_password_submitted,
+ success: false,
+ failure_reason: irs_tracker_failure_reason,
+ )
end
end
@@ -179,7 +225,9 @@
context 'IAL1 user submits valid new password' do
it 'redirects to sign in page' do
stub_analytics
+ stub_attempts_tracker
allow(@analytics).to receive(:track_event)
+ allow(@irs_attempts_api_tracker).to receive(:track_event)
raw_reset_token, db_confirmation_token =
Devise.token_generator.generate(User, :reset_password_token)
@@ -214,7 +262,11 @@
expect(@analytics).to have_received(:track_event).
with('Password Reset: Password Submitted', analytics_hash)
-
+ expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
+ :forgot_password_new_password_submitted,
+ success: true,
+ failure_reason: {},
+ )
expect(user.events.password_changed.size).to be 1
expect(response).to redirect_to new_user_session_path
@@ -227,7 +279,9 @@
context 'ial2 user submits valid new password' do
it 'deactivates the active profile and redirects' do
stub_analytics
+ stub_attempts_tracker
allow(@analytics).to receive(:track_event)
+ allow(@irs_attempts_api_tracker).to receive(:track_event)
raw_reset_token, db_confirmation_token =
Devise.token_generator.generate(User, :reset_password_token)
@@ -258,6 +312,11 @@
expect(@analytics).to have_received(:track_event).
with('Password Reset: Password Submitted', analytics_hash)
+ expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
+ :forgot_password_new_password_submitted,
+ success: true,
+ failure_reason: {},
+ )
expect(user.active_profile.present?).to eq false
@@ -268,7 +327,9 @@
context 'unconfirmed user submits valid new password' do
it 'confirms the user' do
stub_analytics
+ stub_attempts_tracker
allow(@analytics).to receive(:track_event)
+ allow(@irs_attempts_api_tracker).to receive(:track_event)
raw_reset_token, db_confirmation_token =
Devise.token_generator.generate(User, :reset_password_token)
@@ -300,6 +361,11 @@
expect(@analytics).to have_received(:track_event).
with('Password Reset: Password Submitted', analytics_hash)
+ expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
+ :forgot_password_new_password_submitted,
+ success: true,
+ failure_reason: {},
+ )
expect(user.reload.confirmed?).to eq true
diff --git a/spec/controllers/users/rules_of_use_controller_spec.rb b/spec/controllers/users/rules_of_use_controller_spec.rb
index 6b20923ba62..9a903e3ab15 100644
--- a/spec/controllers/users/rules_of_use_controller_spec.rb
+++ b/spec/controllers/users/rules_of_use_controller_spec.rb
@@ -147,7 +147,7 @@
it 'logs a failure analytics event' do
stub_analytics
expect(@analytics).to receive(:track_event).
- with('Rules of Use Submitted', hash_including(success: false))
+ with('Rules of Use Submitted', hash_including(success: false))
action
end
diff --git a/spec/controllers/users/service_provider_revoke_controller_spec.rb b/spec/controllers/users/service_provider_revoke_controller_spec.rb
index e72982aef8a..b8da2b470cc 100644
--- a/spec/controllers/users/service_provider_revoke_controller_spec.rb
+++ b/spec/controllers/users/service_provider_revoke_controller_spec.rb
@@ -67,7 +67,7 @@
subject
end
end.to change { @identity.reload.deleted_at&.to_i }.
- from(nil).to(now.to_i)
+ from(nil).to(now.to_i)
expect(response).to redirect_to(account_connected_accounts_path)
end
diff --git a/spec/controllers/users/two_factor_authentication_controller_spec.rb b/spec/controllers/users/two_factor_authentication_controller_spec.rb
index dfeac6aa2f3..4cd745abb16 100644
--- a/spec/controllers/users/two_factor_authentication_controller_spec.rb
+++ b/spec/controllers/users/two_factor_authentication_controller_spec.rb
@@ -326,13 +326,24 @@ def index
it 'tracks the verification attempt event' do
stub_attempts_tracker
- expect(@irs_attempts_api_tracker).to receive(:mfa_verify_phone_otp_sent).
+ expect(@irs_attempts_api_tracker).to receive(:mfa_login_phone_otp_sent).
with(phone_number: '+12025551212', reauthentication: false, success: true)
get :send_code, params: { otp_delivery_selection_form:
{ otp_delivery_preference: 'sms' } }
end
+ it 'tracks the attempt event when user session context is reauthentication' do
+ stub_attempts_tracker
+ subject.user_session[:context] = 'reauthentication'
+
+ expect(@irs_attempts_api_tracker).to receive(:mfa_login_phone_otp_sent).
+ with(phone_number: '+12025551212', reauthentication: true, success: true)
+
+ get :send_code, params: { otp_delivery_selection_form:
+ { otp_delivery_preference: 'sms' } }
+ end
+
it 'calls OtpRateLimiter#exceeded_otp_send_limit? and #increment' do
otp_rate_limiter = instance_double(OtpRateLimiter)
allow(OtpRateLimiter).to receive(:new).
diff --git a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb
index 366a5b1d62c..866761b0f63 100644
--- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb
+++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb
@@ -73,7 +73,7 @@
expect(form).to receive(:submit).
with(params.require(:two_factor_options_form).permit(:selection)).
and_return(response)
- expect(form).to receive(:selection).and_return(['voice'])
+ expect(form).to receive(:selection).twice.and_return(['voice'])
patch :create, params: voice_params
@@ -102,6 +102,21 @@
}
end
+ it 'tracks IRS attempts event' do
+ stub_sign_in_before_2fa
+ stub_attempts_tracker
+
+ expect(@irs_attempts_api_tracker).to receive(:track_event).
+ with(:mfa_enroll_options_selected, success: true,
+ mfa_device_types: ['voice', 'auth_app'])
+
+ patch :create, params: {
+ two_factor_options_form: {
+ selection: ['voice', 'auth_app'],
+ },
+ }
+ end
+
context 'when the selection is only phone and multi mfa is enabled' do
before do
allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true)
diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb
index 79707620de0..c5b85d9ef3a 100644
--- a/spec/controllers/users/webauthn_setup_controller_spec.rb
+++ b/spec/controllers/users/webauthn_setup_controller_spec.rb
@@ -43,6 +43,7 @@
it 'tracks page visit' do
stub_sign_in
stub_analytics
+ stub_attempts_tracker
expect(@analytics).to receive(:track_event).
with(
@@ -53,6 +54,8 @@
success: true,
)
+ expect(@irs_attempts_api_tracker).not_to receive(:track_event)
+
get :new
end
end
@@ -243,6 +246,11 @@
}
end
it 'should log expected events' do
+ expect(@analytics).to receive(:track_event).with(
+ 'User Registration: User Fully Registered',
+ { mfa_method: 'webauthn_platform' },
+ )
+
expect(@analytics).to receive(:track_event).with(
'Multi-Factor Authentication Setup',
{
@@ -269,6 +277,51 @@
:mfa_enroll_webauthn_platform, success: true
)
+ registration_log = Funnel::Registration::Create.call(user.id)
+ patch :confirm, params: params
+
+ expect(registration_log.reload.first_mfa).to eq 'webauthn_platform'
+ end
+ end
+
+ context 'with attestation response error' do
+ let(:mfa_selections) { ['webauthn_platform'] }
+ let(:params) do
+ {
+ attestation_object: attestation_object,
+ client_data_json: setup_client_data_json,
+ name: 'mykey',
+ platform_authenticator: 'true',
+ }
+ end
+ it 'should log expected events' do
+ allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:3000')
+ allow(WebAuthn::AttestationStatement).to receive(:from).and_raise(StandardError)
+
+ expect(@analytics).to receive(:track_event).with(
+ 'Multi-Factor Authentication Setup',
+ {
+ enabled_mfa_methods_count: 0,
+ errors: { name: [I18n.t(
+ 'errors.webauthn_platform_setup.attestation_error',
+ link: MarketingSite.contact_url,
+ )] },
+ error_details: { name: [I18n.t(
+ 'errors.webauthn_platform_setup.attestation_error',
+ link: MarketingSite.contact_url,
+ )] },
+ in_multi_mfa_selection_flow: true,
+ mfa_method_counts: {},
+ multi_factor_auth_method: 'webauthn_platform',
+ pii_like_keypaths: [[:mfa_method_counts, :phone]],
+ success: false,
+ },
+ )
+
+ expect(@irs_attempts_api_tracker).to receive(:track_event).with(
+ :mfa_enroll_webauthn_platform, success: false
+ )
+
patch :confirm, params: params
end
end
diff --git a/spec/features/account/backup_codes_spec.rb b/spec/features/account/backup_codes_spec.rb
index 7f149813fc3..9bffd3ffbf1 100644
--- a/spec/features/account/backup_codes_spec.rb
+++ b/spec/features/account/backup_codes_spec.rb
@@ -55,8 +55,8 @@
click_continue
generated_at = user.backup_code_configurations.
- order(created_at: :asc).first.created_at.
- in_time_zone('UTC')
+ order(created_at: :asc).first.created_at.
+ in_time_zone('UTC')
formatted_generated_at = l(generated_at, format: t('time.formats.event_timestamp'))
expected_message = "#{t('account.index.backup_codes_exist')} #{formatted_generated_at}"
diff --git a/spec/features/idv/doc_auth/send_link_step_spec.rb b/spec/features/idv/doc_auth/send_link_step_spec.rb
index 0120c24dba5..2cf07639ed2 100644
--- a/spec/features/idv/doc_auth/send_link_step_spec.rb
+++ b/spec/features/idv/doc_auth/send_link_step_spec.rb
@@ -27,6 +27,11 @@
end
it 'proceeds to the next page with valid info' do
+ expect_any_instance_of(IrsAttemptsApi::Tracker).to receive(:track_event).with(
+ :idv_phone_upload_link_sent,
+ success: true,
+ phone_number: '+1 415-555-0199',
+ )
expect(Telephony).to receive(:send_doc_auth_link).
with(hash_including(to: '+1 415-555-0199')).
and_call_original
@@ -44,7 +49,11 @@
impl.call(**config)
end
-
+ expect_any_instance_of(IrsAttemptsApi::Tracker).to receive(:track_event).with(
+ :idv_phone_upload_link_sent,
+ success: true,
+ phone_number: '+1 415-555-0199',
+ )
fill_in :doc_auth_phone, with: '415-555-0199'
click_idv_continue
@@ -59,6 +68,11 @@
end
it 'does not proceed if Telephony raises an error' do
+ expect_any_instance_of(IrsAttemptsApi::Tracker).to receive(:track_event).with(
+ :idv_phone_upload_link_sent,
+ success: false,
+ phone_number: '+1 225-555-1000',
+ )
fill_in :doc_auth_phone, with: '225-555-1000'
click_idv_continue
@@ -127,7 +141,11 @@
allow_any_instance_of(Flow::BaseFlow).to receive(:flow_session).and_return(
document_capture_session_uuid: document_capture_session.uuid,
)
-
+ expect_any_instance_of(IrsAttemptsApi::Tracker).to receive(:track_event).with(
+ :idv_phone_upload_link_sent,
+ success: true,
+ phone_number: '+1 415-555-0199',
+ )
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).to eq('document-capture-session' => document_capture_session.uuid)
diff --git a/spec/features/idv/steps/confirmation_step_spec.rb b/spec/features/idv/steps/confirmation_step_spec.rb
index 5392fd077c5..f306c3050ea 100644
--- a/spec/features/idv/steps/confirmation_step_spec.rb
+++ b/spec/features/idv/steps/confirmation_step_spec.rb
@@ -4,11 +4,14 @@
include IdvStepHelper
let(:idv_api_enabled_steps) { [] }
+ let(:idv_personal_key_confirmation_enabled) { true }
let(:sp) { nil }
let(:address_verification_mechanism) { :phone }
before do
allow(IdentityConfig.store).to receive(:idv_api_enabled_steps).and_return(idv_api_enabled_steps)
+ allow(IdentityConfig.store).to receive(:idv_personal_key_confirmation_enabled).
+ and_return(idv_personal_key_confirmation_enabled)
start_idv_from_sp(sp)
complete_idv_steps_before_confirmation_step(address_verification_mechanism)
end
@@ -47,6 +50,21 @@
acknowledge_and_confirm_personal_key
expect(page).to have_current_path(account_path)
end
+
+ context 'with personal key confirmation disabled' do
+ let(:idv_personal_key_confirmation_enabled) { false }
+
+ before do
+ click_continue if javascript_enabled?
+ end
+
+ it 'does not display modal content. and continues to the account page' do
+ expect(page).not_to have_content t('forms.personal_key.title')
+ expect(page).not_to have_content t('forms.personal_key.instructions')
+ expect(current_path).to eq(account_path)
+ expect(page).to have_content t('headings.account.verified_account')
+ end
+ end
end
context 'verifying by gpo' do
@@ -79,5 +97,19 @@
expect(current_url).to start_with('http://localhost:7654/auth/result')
end
+
+ context 'with personal key confirmation disabled' do
+ let(:idv_personal_key_confirmation_enabled) { false }
+
+ it 'redirects to the completions page and then to the SP' do
+ click_acknowledge_personal_key
+
+ expect(page).to have_current_path(sign_up_completed_path)
+
+ click_agree_and_continue
+
+ expect(current_url).to start_with('http://localhost:7654/auth/result')
+ end
+ end
end
end
diff --git a/spec/features/irs_attempts_api/event_tracking_spec.rb b/spec/features/irs_attempts_api/event_tracking_spec.rb
index 64e9a474027..fa4354b2a87 100644
--- a/spec/features/irs_attempts_api/event_tracking_spec.rb
+++ b/spec/features/irs_attempts_api/event_tracking_spec.rb
@@ -31,12 +31,18 @@
sign_in_user(user)
events = irs_attempts_api_tracked_events(timestamp: Time.zone.now)
- expected_event_types = ['email-and-password-auth', 'mfa-verify-phone-otp-sent']
+ expected_event_types = %w[email-and-password-auth mfa-login-phone-otp-sent]
received_event_types = events.map(&:event_type)
- expect(events.count).to be > 0
- expect(received_event_types.sort).to eq(expected_event_types.sort)
+ expect(events.count).to eq received_event_types.count
+ expect(received_event_types).to match_array(expected_event_types)
+
+ metadata = events.first.event_metadata
+ expect(metadata[:user_ip_address]).to eq '127.0.0.1'
+ expect(metadata[:irs_application_url]).to eq 'http://localhost:7654/auth/result'
+ expect(metadata[:unique_session_id]).to be_a(String)
+ expect(metadata[:success]).to be_truthy
end
end
diff --git a/spec/features/saml/ial2_sso_spec.rb b/spec/features/saml/ial2_sso_spec.rb
index 830bfbbe9b2..78c1f714f6a 100644
--- a/spec/features/saml/ial2_sso_spec.rb
+++ b/spec/features/saml/ial2_sso_spec.rb
@@ -40,10 +40,6 @@ def expected_gpo_return_to_sp_url
).to_s
end
- def mock_gpo_mail_bounced
- allow_any_instance_of(UserDecorator).to receive(:gpo_mail_bounced?).and_return(true)
- end
-
def update_mailing_address
click_on t('idv.buttons.mail.resend')
fill_in t('idv.form.password'), with: user.password
@@ -138,55 +134,6 @@ def sign_out_user
expect(current_path).to eq(idv_come_back_later_path)
end
end
-
- context 'provides an option to update address if undeliverable' do
- it 'allows the user to update the address' do
- user = create(:user, :signed_up)
-
- perform_id_verification_with_gpo_without_confirming_code(user)
-
- expect(current_url).to eq expected_gpo_return_to_sp_url
-
- visit account_path
-
- mock_gpo_mail_bounced
- visit account_path
- click_link(t('account.index.verification.update_address'))
-
- expect(current_path).to eq idv_gpo_path
-
- fill_out_address_form_fail
- click_on t('idv.buttons.mail.resend')
-
- fill_out_address_form_ok
- update_mailing_address
- end
-
- it 'throttles resolution' do
- user = create(:user, :signed_up)
-
- perform_id_verification_with_gpo_without_confirming_code(user)
-
- expect(current_url).to eq expected_gpo_return_to_sp_url
-
- visit account_path
-
- mock_gpo_mail_bounced
- visit account_path
- click_link(t('account.index.verification.update_address'))
-
- expect(current_path).to eq idv_gpo_path
- fill_out_address_form_resolution_fail
- click_on t('idv.buttons.mail.resend')
- expect(current_path).to eq idv_gpo_path
- expect(page).to have_content(t('idv.failure.sessions.heading'))
-
- fill_out_address_form_resolution_fail
- click_on t('idv.buttons.mail.resend')
- expect(current_path).to eq idv_gpo_path
- expect(page).to have_content(strip_tags(t('idv.failure.sessions.heading')))
- end
- end
end
end
diff --git a/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb b/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb
index 4a93d6cb15c..779c49f8fd4 100644
--- a/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb
+++ b/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb
@@ -74,6 +74,37 @@
expect(current_url).to start_with('http://localhost:7654/auth/result')
end
+ context 'when the user needs a backup code reminder' do
+ let!(:user) { create(:user, :signed_up, :with_authentication_app, :with_backup_code) }
+ let!(:event) do
+ create(:event, user: user, event_type: :sign_in_after_2fa, created_at: 9.months.ago)
+ end
+
+ context 'without feature flag on (IdentityConfig.store.backup_code_reminder_redirect)' do
+ it 'redirects the user to the account url' do
+ sign_in_user(user)
+ fill_in_code_with_last_totp(user)
+ click_submit_default
+
+ expect(current_path).to eq account_path
+ end
+ end
+
+ context 'with the feature flag turned on' do
+ before do
+ allow(IdentityConfig.store).to receive(:backup_code_reminder_redirect).and_return(true)
+ end
+
+ it 'redirects the user to the backup code reminder url' do
+ sign_in_user(user)
+ fill_in_code_with_last_totp(user)
+ click_submit_default
+
+ expect(current_path).to eq backup_code_reminder_path
+ end
+ end
+ end
+
def sign_out_user
first(:link, t('links.sign_out')).click
end
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 099ca99e5b4..edd7b0e58b8 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
@@ -150,7 +150,7 @@
scenario 'redirects to the two_factor path with an error and phone option selected' do
expect(page).
- to have_content(t('errors.two_factor_auth_setup.must_select_additional_option'))
+ to have_content(t('errors.two_factor_auth_setup.must_select_additional_option'))
expect(
URI.parse(current_url).path + '#' + URI.parse(current_url).fragment,
).to eq authentication_methods_setup_path(anchor: 'select_phone')
@@ -159,7 +159,7 @@
scenario 'clears the error when another mfa method is selected' do
click_2fa_option('backup_code')
expect(page).
- to_not have_content(t('errors.two_factor_auth_setup.must_select_additional_option'))
+ to_not have_content(t('errors.two_factor_auth_setup.must_select_additional_option'))
end
scenario 'clears the error when phone mfa method is unselected' do
diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb
index 6f34f75befe..1ad82115b2d 100644
--- a/spec/features/users/sign_in_spec.rb
+++ b/spec/features/users/sign_in_spec.rb
@@ -954,7 +954,7 @@
expect(page).to have_content(user.email)
agree_and_continue_button = find_button(t('sign_up.agree_and_continue'))
- action_url = agree_and_continue_button.find(:xpath, '..')[:action]
+ action_url = agree_and_continue_button.ancestor('form')[:action]
agree_and_continue_button.click
expect(current_url).to start_with('http://localhost:7654/auth/result')
diff --git a/spec/fixtures/proofing/lexis_nexis/ddp/request.json b/spec/fixtures/proofing/lexis_nexis/ddp/request.json
index b43d46754e8..f633a9e040f 100644
--- a/spec/fixtures/proofing/lexis_nexis/ddp/request.json
+++ b/spec/fixtures/proofing/lexis_nexis/ddp/request.json
@@ -17,5 +17,8 @@
"policy": "test-policy",
"service_type": "all",
"session_id": "UNIQUE_SESSION_ID",
- "ssn_hash": "15e2b0d3c33891ebb0f1ef609ec419420c20e320ce94c65fbc8c3312448eb225"
+ "ssn_hash": "15e2b0d3c33891ebb0f1ef609ec419420c20e320ce94c65fbc8c3312448eb225",
+ "input_ip_address": "127.0.0.1",
+ "local_attrib_1": "ABCD"
+
}
diff --git a/spec/forms/delete_user_email_form_spec.rb b/spec/forms/delete_user_email_form_spec.rb
index f7972952e8e..36b6cc79483 100644
--- a/spec/forms/delete_user_email_form_spec.rb
+++ b/spec/forms/delete_user_email_form_spec.rb
@@ -54,9 +54,9 @@
email: email_address.email,
)).ordered
expect(PushNotification::HttpPush).to receive(:deliver).once.
- with(PushNotification::RecoveryInformationChangedEvent.new(
- user: user,
- )).ordered
+ with(PushNotification::RecoveryInformationChangedEvent.new(
+ user: user,
+ )).ordered
submit
end
diff --git a/spec/helpers/script_helper_spec.rb b/spec/helpers/script_helper_spec.rb
index 1483415be93..ed1137d6f02 100644
--- a/spec/helpers/script_helper_spec.rb
+++ b/spec/helpers/script_helper_spec.rb
@@ -86,6 +86,25 @@
)
end
+ context 'with script integrity available' do
+ before do
+ allow(AssetSources).to receive(:get_integrity).and_return(nil)
+ allow(AssetSources).to receive(:get_integrity).with('/application.js').
+ and_return('sha256-aztp/wpATyjXXpigZtP8ZP/9mUCHDMaL7OKFRbmnUIazQ9ehNmg4CD5Ljzym/TyA')
+ end
+
+ it 'adds integrity attribute' do
+ output = render_javascript_pack_once_tags
+
+ expect(output).to have_css(
+ "script[src^='/polyfill.js']:not([integrity]) ~ \
+ script[src^='/application.js'][integrity^='sha256-']",
+ count: 1,
+ visible: :all,
+ )
+ end
+ end
+
context 'local development crossorigin sources' do
let(:webpack_port) { '3035' }
diff --git a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx
index 392c4e731c7..f472bcc1972 100644
--- a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx
+++ b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx
@@ -119,7 +119,7 @@ describe('maxAttemptsBeforeNativeCamera logging tests', () => {
*/
it('calls analytics with native camera message when failed attempts is greater than or equal to 0', async function () {
const addPageAction = sinon.spy();
- const acuantCaptureComponent = ;
+ const acuantCaptureComponent = ;
function TestComponent({ children }) {
return (
@@ -142,6 +142,7 @@ describe('maxAttemptsBeforeNativeCamera logging tests', () => {
expect(addPageAction).to.have.been.called();
expect(addPageAction).to.have.been.calledWith(
'IdV: Native camera forced after failed attempts',
+ { field: 'example', failed_attempts: 0 },
);
});
diff --git a/spec/javascripts/packs/form-validation-spec.js b/spec/javascripts/packs/form-validation-spec.js
deleted file mode 100644
index acb6805ab23..00000000000
--- a/spec/javascripts/packs/form-validation-spec.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import { screen } from '@testing-library/dom';
-import userEvent from '@testing-library/user-event';
-import { initialize } from '../../../app/javascript/packs/form-validation';
-
-describe('form-validation', () => {
- const onSubmit = (event) => event.preventDefault();
-
- beforeEach(() => {
- window.addEventListener('submit', onSubmit);
- });
-
- afterEach(() => {
- window.removeEventListener('submit', onSubmit);
- });
-
- it('adds active, disabled effect to submit buttons on submit', () => {
- document.body.innerHTML = `
-
-
- `;
-
- const submit1 = screen.getByText('Submit1');
- const submit2 = screen.getByText('Submit2');
- const submit3 = screen.getByText('Submit3');
- const button1 = screen.getByText('Button1');
- const input1 = screen.getByDisplayValue('Input1');
-
- const form = submit1.closest('form');
- initialize(form);
- submit1.click();
-
- expect(submit1.disabled).to.be.true();
- expect(submit1.classList.contains('usa-button--active')).to.be.true();
- expect(submit2.disabled).to.be.true();
- expect(submit2.classList.contains('usa-button--active')).to.be.true();
- expect(submit3.disabled).to.be.true();
- expect(submit3.classList.contains('usa-button--active')).to.be.true();
- expect(button1.disabled).to.be.false();
- expect(button1.classList.contains('usa-button--active')).to.be.false();
- expect(input1.disabled).to.be.false();
- expect(input1.classList.contains('usa-button--active')).to.be.false();
- });
-
- it('checks validity of inputs', async () => {
- document.body.innerHTML = `
- `;
-
- initialize(document.querySelector('form'));
-
- const notRequiredField = screen.getByLabelText('not required field');
- await userEvent.type(notRequiredField, 'a{Backspace}');
- expect(notRequiredField.validationMessage).to.be.empty();
-
- const requiredField = screen.getByLabelText('required field');
- await userEvent.type(requiredField, 'a{Backspace}');
- expect(requiredField.validationMessage).to.equal('simple_form.required.text');
- await userEvent.type(requiredField, 'a');
- expect(notRequiredField.validationMessage).to.be.empty();
- });
-
- it('resets its own custom validity message on input', async () => {
- document.body.innerHTML = `
- `;
-
- const form = document.querySelector('form');
- initialize(form);
-
- form.checkValidity();
-
- const input = screen.getByLabelText('required field');
- await userEvent.type(input, 'a');
-
- expect(input.validity.customError).to.be.false();
- });
-
- it('does not reset external custom validity message on input', async () => {
- document.body.innerHTML = `
- `;
-
- const form = document.querySelector('form');
- initialize(form);
-
- form.checkValidity();
-
- /** @type {HTMLInputElement} */
- const input = screen.getByLabelText('field');
- input.setCustomValidity('custom error');
-
- await userEvent.type(input, 'a');
-
- expect(input.validity.customError).to.be.true();
- });
-});
diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb
index d7f9dc0503d..fd3c863fc30 100644
--- a/spec/jobs/resolution_proofing_job_spec.rb
+++ b/spec/jobs/resolution_proofing_job_spec.rb
@@ -39,9 +39,11 @@
instance_double(Proofing::Aamva::Proofer, class: Proofing::Aamva::Proofer)
end
let(:trace_id) { SecureRandom.uuid }
- let(:user) { build(:user, :signed_up) }
+ let(:user) { create(:user, :signed_up) }
let(:threatmetrix_session_id) { SecureRandom.uuid }
let(:threatmetrix_request_id) { Proofing::Mock::DdpMockClient::TRANSACTION_ID }
+ let(:request_ip) { '127.0.0.1' }
+ let(:uuid_prefix) { 'ABC' }
describe '.perform_later' do
it 'stores results' do
@@ -53,6 +55,8 @@
trace_id: trace_id,
user_id: user.id,
threatmetrix_session_id: threatmetrix_session_id,
+ request_ip: request_ip,
+ uuid_prefix: uuid_prefix,
)
result = document_capture_session.load_proofing_result[:result]
@@ -72,6 +76,8 @@
trace_id: trace_id,
user_id: user.id,
threatmetrix_session_id: threatmetrix_session_id,
+ request_ip: request_ip,
+ uuid_prefix: uuid_prefix,
)
end
@@ -117,7 +123,7 @@
let(:dob_year_only) { false }
- it 'returns results' do
+ it 'returns results and adds threatmetrix proofing components' do
perform
result = document_capture_session.load_proofing_result[:result]
@@ -156,6 +162,9 @@
threatmetrix_success: true,
threatmetrix_request_id: threatmetrix_request_id,
)
+ proofing_component = user.proofing_component
+ expect(proofing_component.threatmetrix).to equal(true)
+ expect(proofing_component.threatmetrix_review_status).to eq('pass')
end
context 'dob_year_only, failed response from lexisnexis' do
@@ -236,6 +245,15 @@
)
end
end
+
+ context 'no threatmetrix_session_id' do
+ let(:threatmetrix_session_id) { nil }
+ it 'does not attempt to create a ddp proofer' do
+ perform
+
+ expect(instance).not_to receive(:lexisnexis_ddp_proofer)
+ end
+ end
end
context 'stubbing vendors' do
@@ -258,7 +276,7 @@
expect(instance).to receive(:logger_info_hash).ordered.with(
hash_including(
name: 'ThreatMetrix',
- user_id: nil,
+ user_id: user.uuid,
threatmetrix_request_id: Proofing::Mock::DdpMockClient::TRANSACTION_ID,
threatmetrix_success: true,
),
@@ -273,6 +291,10 @@
)
perform
+
+ proofing_component = user.proofing_component
+ expect(proofing_component.threatmetrix).to equal(true)
+ expect(proofing_component.threatmetrix_review_status).to eq('pass')
end
end
diff --git a/spec/lib/asset_sources_spec.rb b/spec/lib/asset_sources_spec.rb
index 70c369625fc..9c640345e2b 100644
--- a/spec/lib/asset_sources_spec.rb
+++ b/spec/lib/asset_sources_spec.rb
@@ -41,6 +41,9 @@
]
}
}
+ },
+ "integrity": {
+ "vendor.js": "sha256-aztp/wpATyjXXpigZtP8ZP/9mUCHDMaL7OKFRbmnUIazQ9ehNmg4CD5Ljzym/TyA"
}
}
STR
@@ -136,6 +139,23 @@
end
end
+ describe '.get_integrity' do
+ let(:path) { 'vendor.js' }
+ subject(:integrity) { AssetSources.get_integrity(path) }
+
+ it 'returns the integrity hash' do
+ expect(integrity).to start_with('sha256-')
+ end
+
+ context 'a path which does not exist in the manifest' do
+ let(:path) { 'missing.js' }
+
+ it 'returns nil' do
+ expect(integrity).to be_nil
+ end
+ end
+ end
+
describe '.load_manifest' do
it 'sets the manifest' do
AssetSources.load_manifest
diff --git a/spec/lib/telephony/pinpoint/voice_sender_spec.rb b/spec/lib/telephony/pinpoint/voice_sender_spec.rb
index e440cd5f7a7..cd88cd99cc1 100644
--- a/spec/lib/telephony/pinpoint/voice_sender_spec.rb
+++ b/spec/lib/telephony/pinpoint/voice_sender_spec.rb
@@ -13,12 +13,12 @@
def mock_build_client
allow(voice_sender).
- to receive(:build_client).with(voice_config).and_return(pinpoint_client)
+ to receive(:build_client).with(voice_config).and_return(pinpoint_client)
end
def mock_build_backup_client
allow(voice_sender).
- to receive(:build_client).with(backup_voice_config).and_return(backup_pinpoint_client)
+ to receive(:build_client).with(backup_voice_config).and_return(backup_pinpoint_client)
end
describe '#send' do
diff --git a/spec/lib/telephony/telephony_spec.rb b/spec/lib/telephony/telephony_spec.rb
index 0c0e88855a7..1c71c6005ac 100644
--- a/spec/lib/telephony/telephony_spec.rb
+++ b/spec/lib/telephony/telephony_spec.rb
@@ -62,7 +62,7 @@
random_double_character = Telephony::GSM_DOUBLE_CHARACTERS.to_a.sample
expect(Telephony.sms_character_length("abc\n¥ΔΦΓΛΩΠΨΣΘΞ#{random_double_character}")).
- to eq 17
+ to eq 17
end
it 'calculates correct length of messages containing non-GSM characters' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index bea5bb3e68c..c50d5e9145f 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -14,12 +14,12 @@
it { is_expected.to have_many(:in_person_enrollments).dependent(:destroy) }
it {
is_expected.to have_one(:pending_in_person_enrollment).
- conditions(status: :pending).
- order(created_at: :desc).
- class_name('InPersonEnrollment').
- with_foreign_key(:user_id).
- inverse_of(:user).
- dependent(:destroy)
+ conditions(status: :pending).
+ order(created_at: :desc).
+ class_name('InPersonEnrollment').
+ with_foreign_key(:user_id).
+ inverse_of(:user).
+ dependent(:destroy)
}
end
diff --git a/spec/presenters/additional_mfa_required_presenter_spec.rb b/spec/presenters/additional_mfa_required_presenter_spec.rb
index 4b8461bdf74..2cda8a0ac0e 100644
--- a/spec/presenters/additional_mfa_required_presenter_spec.rb
+++ b/spec/presenters/additional_mfa_required_presenter_spec.rb
@@ -87,7 +87,7 @@
it 'should return false' do
expect(presenter.cant_skip_anymore?).
- to be_falsey
+ to be_falsey
end
end
@@ -100,7 +100,7 @@
it 'should return true' do
expect(presenter.cant_skip_anymore?).
- to be_truthy
+ to be_truthy
end
end
end
diff --git a/spec/presenters/two_factor_authentication/phone_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/phone_selection_presenter_spec.rb
index 3853ac75c71..425dc3f25d2 100644
--- a/spec/presenters/two_factor_authentication/phone_selection_presenter_spec.rb
+++ b/spec/presenters/two_factor_authentication/phone_selection_presenter_spec.rb
@@ -63,12 +63,12 @@
it 'includes a note to select an additional mfa method on first setup' do
expect(presenter_without_mfa.info).
- to eq(t('two_factor_authentication.two_factor_choice_options.phone_info_html'))
+ to eq(t('two_factor_authentication.two_factor_choice_options.phone_info_html'))
end
it 'does not include a note to select an additional mfa on additional setup' do
expect(presenter_with_mfa.info).
- to eq(t('two_factor_authentication.two_factor_choice_options.phone_info'))
+ to eq(t('two_factor_authentication.two_factor_choice_options.phone_info'))
end
end
end
diff --git a/spec/services/analytics_spec.rb b/spec/services/analytics_spec.rb
index dc02d7e9c97..76d64af4250 100644
--- a/spec/services/analytics_spec.rb
+++ b/spec/services/analytics_spec.rb
@@ -78,7 +78,7 @@
}
expect(ahoy).to receive(:track).
- with('Trackable Event', analytics_hash.merge(request_attributes))
+ with('Trackable Event', analytics_hash.merge(request_attributes))
analytics.track_event('Trackable Event', { success: false })
end
diff --git a/spec/services/cloud_front_header_parser_spec.rb b/spec/services/cloud_front_header_parser_spec.rb
new file mode 100644
index 00000000000..159bd2df848
--- /dev/null
+++ b/spec/services/cloud_front_header_parser_spec.rb
@@ -0,0 +1,46 @@
+require 'rails_helper'
+
+RSpec.describe CloudFrontHeaderParser do
+ let(:req) { ActionDispatch::TestRequest.new({}) }
+ let(:port) { '1234' }
+
+ subject { described_class.new(req) }
+
+ context 'with an IPv4 address' do
+ let(:ip) { '192.0.2.1' }
+
+ before do
+ req.headers['CloudFront-Viewer-Address'] = "#{ip}:#{port}"
+ end
+
+ describe '#client_port' do
+ it 'returns the client port number' do
+ expect(subject.client_port).to eq port
+ end
+ end
+ end
+
+ context 'with an IPv6 address' do
+ let(:ip) { '[2001:DB8::1]' }
+
+ before do
+ req.headers['CloudFront-Viewer-Address'] = "#{ip}:#{port}"
+ end
+
+ describe '#client_port' do
+ it 'returns the client port number' do
+ expect(subject.client_port).to eq port
+ end
+ end
+ end
+
+ context 'with no CloudFront header sent' do
+ let(:ip) { '192.0.2.1' }
+
+ describe '#client_port' do
+ it 'returns nil' do
+ expect(subject.client_port).to eq nil
+ end
+ end
+ end
+end
diff --git a/spec/services/doc_auth/acuant/acuant_client_spec.rb b/spec/services/doc_auth/acuant/acuant_client_spec.rb
index 838368865ae..b9f2e856e8b 100644
--- a/spec/services/doc_auth/acuant/acuant_client_spec.rb
+++ b/spec/services/doc_auth/acuant/acuant_client_spec.rb
@@ -258,11 +258,11 @@
context 'when the result is a pass' do
it 'sends the requests and returns success' do
get_face_stub = stub_request(:get, get_face_image_url).
- to_return(body: AcuantFixtures.get_face_image_response)
+ to_return(body: AcuantFixtures.get_face_image_response)
facial_match_stub = stub_request(:post, full_facial_match_url).
- to_return(body: AcuantFixtures.facial_match_response_success)
+ to_return(body: AcuantFixtures.facial_match_response_success)
liveness_stub = stub_request(:post, full_liveness_url).
- to_return(body: AcuantFixtures.liveness_response_success)
+ to_return(body: AcuantFixtures.liveness_response_success)
result = subject.post_selfie(
instance_id: instance_id,
diff --git a/spec/services/doc_auth/acuant/requests/facial_match_request_spec.rb b/spec/services/doc_auth/acuant/requests/facial_match_request_spec.rb
index 8d3190e95df..bf3be974311 100644
--- a/spec/services/doc_auth/acuant/requests/facial_match_request_spec.rb
+++ b/spec/services/doc_auth/acuant/requests/facial_match_request_spec.rb
@@ -32,8 +32,8 @@
it 'returns a successful response' do
request_stub = stub_request(:post, url).
- with(body: request_body).
- to_return(body: response_body)
+ with(body: request_body).
+ to_return(body: response_body)
response = described_class.new(
config: config,
@@ -53,8 +53,8 @@
it 'returns an unsuccessful response' do
request_stub = stub_request(:post, url).
- with(body: request_body).
- to_return(body: response_body)
+ with(body: request_body).
+ to_return(body: response_body)
response = described_class.new(
config: config,
diff --git a/spec/services/doc_auth/acuant/requests/liveness_request_spec.rb b/spec/services/doc_auth/acuant/requests/liveness_request_spec.rb
index 0b7925e884b..d5c76f65f2e 100644
--- a/spec/services/doc_auth/acuant/requests/liveness_request_spec.rb
+++ b/spec/services/doc_auth/acuant/requests/liveness_request_spec.rb
@@ -34,8 +34,8 @@
it 'returns a successful response' do
request_stub = stub_request(:post, url).
- with(body: request_body).
- to_return(body: response_body)
+ with(body: request_body).
+ to_return(body: response_body)
response = request.fetch
@@ -51,8 +51,8 @@
it 'returns an unsuccessful response' do
request_stub = stub_request(:post, url).
- with(body: request_body).
- to_return(body: response_body)
+ with(body: request_body).
+ to_return(body: response_body)
response = request.fetch
diff --git a/spec/services/id_token_builder_spec.rb b/spec/services/id_token_builder_spec.rb
index 43224eb36e4..d917a4383ba 100644
--- a/spec/services/id_token_builder_spec.rb
+++ b/spec/services/id_token_builder_spec.rb
@@ -95,7 +95,7 @@
it 'sets the code hash correctly' do
leftmost_128_bits = Digest::SHA256.digest(code).
- byteslice(0, IdTokenBuilder::NUM_BYTES_FIRST_128_BITS)
+ byteslice(0, IdTokenBuilder::NUM_BYTES_FIRST_128_BITS)
expected_hash = Base64.urlsafe_encode64(leftmost_128_bits, padding: false)
expect(decoded_payload[:c_hash]).to eq(expected_hash)
diff --git a/spec/services/identity_linker_spec.rb b/spec/services/identity_linker_spec.rb
index 725835281fb..346b73cb011 100644
--- a/spec/services/identity_linker_spec.rb
+++ b/spec/services/identity_linker_spec.rb
@@ -18,8 +18,8 @@
}
identity_attributes = last_identity.attributes.symbolize_keys.
- except(:created_at, :updated_at, :id, :session_uuid,
- :last_authenticated_at, :nonce)
+ except(:created_at, :updated_at, :id, :session_uuid,
+ :last_authenticated_at, :nonce)
expect(last_identity.session_uuid).to match(/.{8}-.{4}-.{4}-.{4}-.{12}/)
expect(last_identity.last_authenticated_at).to be_present
diff --git a/spec/services/idv/in_person_config_spec.rb b/spec/services/idv/in_person_config_spec.rb
index 8abaefe0bd0..69c8a0d036a 100644
--- a/spec/services/idv/in_person_config_spec.rb
+++ b/spec/services/idv/in_person_config_spec.rb
@@ -2,6 +2,7 @@
describe Idv::InPersonConfig do
let(:in_person_proofing_enabled) { false }
+ let(:idv_sp_required) { false }
let(:in_person_proofing_enabled_issuers) { [] }
before do
@@ -9,6 +10,7 @@
and_return(in_person_proofing_enabled)
allow(IdentityConfig.store).to receive(:in_person_proofing_enabled_issuers).
and_return(in_person_proofing_enabled_issuers)
+ allow(IdentityConfig.store).to receive(:idv_sp_required).and_return(idv_sp_required)
end
describe '.enabled_for_issuer?' do
@@ -22,6 +24,12 @@
it { expect(enabled_for_issuer).to eq true }
+ context 'with idv sp required' do
+ let(:idv_sp_required) { true }
+
+ it { expect(enabled_for_issuer).to eq false }
+ end
+
context 'with issuer argument' do
let(:issuer) { 'example-issuer' }
@@ -48,6 +56,18 @@
end
end
+ describe '.enabled_without_issuer?' do
+ subject(:enabled_without_issuer) { described_class.enabled_without_issuer? }
+
+ it { expect(enabled_without_issuer).to eq true }
+
+ context 'with idv sp required' do
+ let(:idv_sp_required) { true }
+
+ it { expect(enabled_without_issuer).to eq false }
+ end
+ end
+
describe '.enabled_issuers' do
subject(:enabled_issuers) { described_class.enabled_issuers }
diff --git a/spec/services/idv/steps/ipp/ssn_step_spec.rb b/spec/services/idv/steps/ipp/ssn_step_spec.rb
index 9221e8ae202..5ad89face08 100644
--- a/spec/services/idv/steps/ipp/ssn_step_spec.rb
+++ b/spec/services/idv/steps/ipp/ssn_step_spec.rb
@@ -46,13 +46,13 @@
end
context 'with proofing device profiling collecting enabled' do
- it 'adds a session id to flow session' do
+ it 'does not add a threatmetrix session id to flow session' do
allow(IdentityConfig.store).
to receive(:proofing_device_profiling_collecting_enabled).
and_return(true)
step.extra_view_variables
- expect(flow.flow_session[:threatmetrix_session_id]).to_not eq(nil)
+ expect(flow.flow_session[:threatmetrix_session_id]).to eq(nil)
end
it 'does not change threatmetrix_session_id when updating ssn' do
diff --git a/spec/services/irs_attempts_api/attempt_event_spec.rb b/spec/services/irs_attempts_api/attempt_event_spec.rb
index b1753f6747c..547219e62fc 100644
--- a/spec/services/irs_attempts_api/attempt_event_spec.rb
+++ b/spec/services/irs_attempts_api/attempt_event_spec.rb
@@ -7,7 +7,7 @@
before do
encoded_public_key = Base64.strict_encode64(irs_attempt_api_public_key.to_der)
allow(IdentityConfig.store).to receive(:irs_attempt_api_public_key).
- and_return(encoded_public_key)
+ and_return(encoded_public_key)
end
let(:jti) { 'test-unique-id' }
diff --git a/spec/services/irs_attempts_api/envelope_encryptor_spec.rb b/spec/services/irs_attempts_api/envelope_encryptor_spec.rb
new file mode 100644
index 00000000000..8b7cba7c1da
--- /dev/null
+++ b/spec/services/irs_attempts_api/envelope_encryptor_spec.rb
@@ -0,0 +1,60 @@
+require 'rails_helper'
+RSpec.describe IrsAttemptsApi::EnvelopeEncryptor do
+ let(:private_key) { OpenSSL::PKey::RSA.new(4096) }
+ let(:public_key) { private_key.public_key }
+ describe '.encrypt' do
+ it 'returns encrypted result' do
+ text = 'test'
+ time = Time.zone.now
+ result = IrsAttemptsApi::EnvelopeEncryptor.encrypt(
+ data: text, timestamp: time, public_key: public_key,
+ )
+
+ expect(result.encrypted_data).to_not eq text
+ end
+
+ it 'filename includes digest and truncated timestamp' do
+ text = 'test'
+ time = Time.zone.now
+ result = IrsAttemptsApi::EnvelopeEncryptor.encrypt(
+ data: text, timestamp: time,
+ public_key: public_key
+ )
+ digest = Digest::SHA256.hexdigest(result.encrypted_data)
+
+ expect(result.filename).to include(
+ IrsAttemptsApi::EnvelopeEncryptor.formatted_timestamp(time),
+ )
+ expect(result.filename).to include(digest)
+ end
+ end
+
+ describe '.decrypt' do
+ it 'returns decrypted text' do
+ text = 'test'
+ time = Time.zone.now
+ result = IrsAttemptsApi::EnvelopeEncryptor.encrypt(
+ data: text, timestamp: time,
+ public_key: public_key
+ )
+ key = private_key.private_decrypt(result.encrypted_key)
+
+ expect(
+ IrsAttemptsApi::EnvelopeEncryptor.decrypt(
+ encrypted_data: result.encrypted_data,
+ key: key,
+ iv: result.iv,
+ ),
+ ).to eq(text)
+ end
+ end
+
+ describe '.formatted_timestamp' do
+ it 'formats according to the specification' do
+ timestamp = Time.new(2022, 1, 1, 11, 1, 1, 'UTC')
+ result = IrsAttemptsApi::EnvelopeEncryptor.formatted_timestamp(timestamp)
+
+ expect(result).to eq '20220101T11Z'
+ end
+ end
+end
diff --git a/spec/services/irs_attempts_api/tracker_spec.rb b/spec/services/irs_attempts_api/tracker_spec.rb
index 8b03cfaaf49..e3826510a16 100644
--- a/spec/services/irs_attempts_api/tracker_spec.rb
+++ b/spec/services/irs_attempts_api/tracker_spec.rb
@@ -7,6 +7,9 @@
)
allow(request).to receive(:user_agent).and_return('example/1.0')
allow(request).to receive(:remote_ip).and_return('192.0.2.1')
+ allow(request).to receive(:headers).and_return(
+ { 'CloudFront-Viewer-Address' => '192.0.2.1:1234' },
+ )
end
let(:irs_attempt_api_enabled) { true }
@@ -14,7 +17,7 @@
let(:enabled_for_session) { true }
let(:request) { instance_double(ActionDispatch::Request) }
let(:service_provider) { create(:service_provider) }
- let(:device_fingerprint) { 'device_id' }
+ let(:cookie_device_uuid) { 'device_id' }
let(:sp_request_uri) { 'https://example.com/auth_page' }
let(:user) { create(:user) }
@@ -24,7 +27,7 @@
request: request,
user: user,
sp: service_provider,
- device_fingerprint: device_fingerprint,
+ cookie_device_uuid: cookie_device_uuid,
sp_request_uri: sp_request_uri,
enabled_for_session: enabled_for_session,
)
diff --git a/spec/services/marketing_site_spec.rb b/spec/services/marketing_site_spec.rb
index 503e4c78035..b77196acfc0 100644
--- a/spec/services/marketing_site_spec.rb
+++ b/spec/services/marketing_site_spec.rb
@@ -50,7 +50,7 @@
describe '.rules_of_use_url' do
it 'points to the rules of use page' do
expect(MarketingSite.rules_of_use_url).
- to eq('https://www.login.gov/policy/rules-of-use/')
+ to eq('https://www.login.gov/policy/rules-of-use/')
end
context 'when the user has set their locale to :es' do
@@ -58,7 +58,7 @@
it 'points to the rules of use page with the locale appended' do
expect(MarketingSite.rules_of_use_url).
- to eq('https://www.login.gov/es/policy/rules-of-use/')
+ to eq('https://www.login.gov/es/policy/rules-of-use/')
end
end
end
diff --git a/spec/services/proofing/aamva/request/security_token_request_spec.rb b/spec/services/proofing/aamva/request/security_token_request_spec.rb
index c3e4ee61def..d2fd8923951 100644
--- a/spec/services/proofing/aamva/request/security_token_request_spec.rb
+++ b/spec/services/proofing/aamva/request/security_token_request_spec.rb
@@ -31,9 +31,9 @@
expect(signature.text).to_not be_empty
body_without_sig = security_token_request.body.
- gsub(public_key.text, '').
- gsub(signature.text, '').
- gsub(key_identifier.text, '')
+ gsub(public_key.text, '').
+ gsub(signature.text, '').
+ gsub(key_identifier.text, '')
expect(body_without_sig).to eq(AamvaFixtures.security_token_request)
end
diff --git a/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb b/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb
index 0b5e2588cf7..66a55915298 100644
--- a/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb
+++ b/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb
@@ -16,6 +16,8 @@
threatmetrix_session_id: '123456',
phone: '5551231234',
email: 'test@example.com',
+ request_ip: '127.0.0.1',
+ uuid_prefix: 'ABCD',
}
end
let(:verification_request) do
diff --git a/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb b/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb
index ec7c7d71c91..9b09b30e77b 100644
--- a/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb
+++ b/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb
@@ -17,6 +17,8 @@
threatmetrix_session_id: 'UNIQUE_SESSION_ID',
phone: '5551231234',
email: 'test@example.com',
+ request_ip: '127.0.0.1',
+ uuid_prefix: 'ABCD',
}
end
diff --git a/spec/services/proofing/mock/ddp_mock_client_spec.rb b/spec/services/proofing/mock/ddp_mock_client_spec.rb
new file mode 100644
index 00000000000..ee5ae3a21e5
--- /dev/null
+++ b/spec/services/proofing/mock/ddp_mock_client_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+RSpec.describe Proofing::Mock::DdpMockClient do
+ let(:applicant) {
+ Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN.merge(threatmetrix_session_id: 'ABCD-1234')
+ }
+
+ subject(:instance) { described_class.new }
+
+ describe '#proof' do
+ subject(:result) { instance.proof(applicant) }
+
+ it 'passes by default' do
+ expect(result.review_status).to eq('pass')
+ end
+
+ context 'with magic "reject" SSN' do
+ let(:applicant) { super().merge(ssn: '666-77-8888') }
+ it 'fails' do
+ expect(result.review_status).to eq('reject')
+ end
+ end
+
+ context 'with magic "review" SSN' do
+ let(:applicant) { super().merge(ssn: '666-77-9999') }
+ it 'fails' do
+ expect(result.review_status).to eq('review')
+ end
+ end
+
+ context 'with magic "nil" SSN' do
+ let(:applicant) { super().merge(ssn: '666-77-0000') }
+ it 'fails' do
+ expect(result.review_status).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/simplecov_helper.rb b/spec/simplecov_helper.rb
index 77bea2b9ed3..3a327d3a971 100644
--- a/spec/simplecov_helper.rb
+++ b/spec/simplecov_helper.rb
@@ -60,9 +60,6 @@ def self.configured_formatters
if ENV['GITLAB_CI']
# GitLab CI uses Cobertura formatter to display diffs in pull requests
formatters << SimpleCov::Formatter::CoberturaFormatter
- elsif ENV['CIRCLE_CI']
- # CircleCI uses JSON formatting for CodeClimate
- formatters << SimpleCov::Formatter::JSONFormatter
end
formatters
diff --git a/spec/support/controller_helper.rb b/spec/support/controller_helper.rb
index 6ee1590361f..bbe859febbd 100644
--- a/spec/support/controller_helper.rb
+++ b/spec/support/controller_helper.rb
@@ -70,10 +70,6 @@ def stub_decorated_user_with_pending_profile(user)
decorated_user
end
- def stub_gpo_mail_bounced(decorated_user)
- allow(decorated_user).to receive(:gpo_mail_bounced?).and_return(true)
- end
-
def stub_identity(user, params)
ServiceProviderIdentity.new(params.merge(user: user)).save
end
diff --git a/spec/support/shared_examples_for_email_validation.rb b/spec/support/shared_examples_for_email_validation.rb
index 32e2919b461..9f287cd4aa9 100644
--- a/spec/support/shared_examples_for_email_validation.rb
+++ b/spec/support/shared_examples_for_email_validation.rb
@@ -1,7 +1,7 @@
shared_examples 'email validation' do
it 'uses the valid_email gem with mx and ban_disposable options' do
email_validator = subject._validators.values.flatten.
- detect { |v| v.instance_of?(EmailValidator) }
+ detect { |v| v.instance_of?(EmailValidator) }
expect(email_validator.options).
to eq(mx_with_fallback: true, ban_disposable_email: true)
diff --git a/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb b/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb
index 6dead571ecb..7024b083bc3 100644
--- a/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb
+++ b/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb
@@ -22,7 +22,7 @@
render
expect(rendered).to have_link(t('account.index.auth_app_add'), href: authenticator_setup_url)
- expect(rendered).not_to have_xpath("//input[@value='Disable']")
+ expect(rendered).not_to have_link(t('forms.buttons.disable'))
end
end
@@ -43,7 +43,10 @@
it 'contains link to disable TOTP' do
render
- expect(rendered).to have_link(t('forms.buttons.disable', href: auth_app_delete_path))
+ expect(rendered).to have_link(
+ t('forms.buttons.disable'),
+ href: auth_app_delete_path(id: user.auth_app_configurations.first.id),
+ )
end
end
@@ -95,7 +98,7 @@
it 'disables delete buttons for the last non restricted mfa method with phone configured' do
render
- expect(rendered).to_not have_link(t('forms.buttons.disable', href: auth_app_delete_path))
+ expect(rendered).to_not have_link(t('forms.buttons.disable'))
end
end
end
diff --git a/spec/views/idv/gpo/index.html.erb_spec.rb b/spec/views/idv/gpo/index.html.erb_spec.rb
index 2188c648e35..a627f5c332d 100644
--- a/spec/views/idv/gpo/index.html.erb_spec.rb
+++ b/spec/views/idv/gpo/index.html.erb_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
describe 'idv/gpo/index.html.erb' do
- let(:gpo_mail_bounced) { false }
let(:letter_already_sent) { false }
let(:user_needs_address_otp_verification) { false }
let(:go_back_path) { nil }
@@ -13,7 +12,6 @@
before do
allow(view).to receive(:go_back_path).and_return(go_back_path)
- allow(presenter).to receive(:gpo_mail_bounced?).and_return(gpo_mail_bounced)
allow(presenter).to receive(:letter_already_sent?).and_return(letter_already_sent)
allow(presenter).to receive(:user_needs_address_otp_verification?).
and_return(user_needs_address_otp_verification)
@@ -39,16 +37,6 @@
end
end
- context 'gpo mail bounced' do
- let(:gpo_mail_bounced) { true }
-
- it 'renders address form to resend letter' do
- expect(rendered).to have_content(I18n.t('idv.messages.gpo.new_address'))
- expect(rendered).to have_field(t('idv.form.address1'))
- expect(rendered).to have_button(I18n.t('idv.buttons.mail.resend'))
- end
- end
-
context 'letter already sent' do
let(:letter_already_sent) { true }
diff --git a/spec/views/idv/shared/_ssn.html.erb_spec.rb b/spec/views/idv/shared/_ssn.html.erb_spec.rb
index 80aa8b701be..c57ae368127 100644
--- a/spec/views/idv/shared/_ssn.html.erb_spec.rb
+++ b/spec/views/idv/shared/_ssn.html.erb_spec.rb
@@ -51,6 +51,17 @@
expect_noscript_tag_rendered
end
end
+
+ context 'session id not specified' do
+ let(:session_id) { nil }
+
+ it 'does not render