diff --git a/app/assets/stylesheets/components/_btn.scss b/app/assets/stylesheets/components/_btn.scss index 5ce81998618..1208f91f830 100644 --- a/app/assets/stylesheets/components/_btn.scss +++ b/app/assets/stylesheets/components/_btn.scss @@ -7,10 +7,12 @@ margin-right: 0; } +// Upstream: https://github.com/uswds/uswds/pull/5631 .usa-button--unstyled { // Temporary: To be backported to design system. Unstyled buttons should inherit the appearance // of a link. display: inline; + width: auto; } .usa-button:disabled.usa-button--active, diff --git a/app/assets/stylesheets/components/_index.scss b/app/assets/stylesheets/components/_index.scss index 9ff1d8eb4b7..0c37bdfb6b8 100644 --- a/app/assets/stylesheets/components/_index.scss +++ b/app/assets/stylesheets/components/_index.scss @@ -13,7 +13,6 @@ @forward 'hr'; @forward 'icon'; @forward 'language-picker'; -@forward 'list'; @forward 'modal'; @forward 'nav'; @forward 'page-heading'; diff --git a/app/assets/stylesheets/components/_list.scss b/app/assets/stylesheets/components/_list.scss deleted file mode 100644 index 4e69fe67efd..00000000000 --- a/app/assets/stylesheets/components/_list.scss +++ /dev/null @@ -1,17 +0,0 @@ -.success-bullets { - .success-bullet { - padding: 1rem 1rem 1rem 0; - - &::before { - background-image: url('/alert/success.svg'); - background-repeat: no-repeat; - content: ''; - float: left; - height: 1rem; - margin-top: 0.33rem; - padding-right: 1.5rem; - vertical-align: middle; - width: 1rem; - } - } -} diff --git a/app/components/icon_list_component.rb b/app/components/icon_list_component.rb index 58a37885fd0..72b468f86ee 100644 --- a/app/components/icon_list_component.rb +++ b/app/components/icon_list_component.rb @@ -19,11 +19,12 @@ def css_class end class IconListItemComponent < BaseComponent - attr_reader :icon, :color + attr_reader :icon, :color, :tag_options - def initialize(icon:, color:) + def initialize(icon:, color:, **tag_options) @icon = icon @color = color + @tag_options = tag_options end def icon_css_class diff --git a/app/components/icon_list_item_component.html.erb b/app/components/icon_list_item_component.html.erb index 05bab773005..128a16fccb8 100644 --- a/app/components/icon_list_item_component.html.erb +++ b/app/components/icon_list_item_component.html.erb @@ -1,6 +1,6 @@ -
  • +<%= content_tag(:li, **tag_options, class: [*tag_options[:class], 'usa-icon-list__item']) do %> <%= content_tag(:div, class: icon_css_class) do %> <%= render IconComponent.new(icon: icon) %> <% end %>
    <%= content %>
    -
  • +<% end %> diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a22eddd567c..a3ebcfb457f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -198,7 +198,7 @@ def fix_broken_personal_key_url if pii_unlocked cacher = Pii::Cacher.new(current_user, user_session) profile = current_user.active_profile - user_session[:personal_key] = profile.encrypt_recovery_pii(cacher.fetch) + user_session[:personal_key] = profile.encrypt_recovery_pii(cacher.fetch(profile.id)) profile.save! analytics.broken_personal_key_regenerated diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index f361f25999d..2421aafd1c4 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -176,7 +176,6 @@ def process_async_state(current_async_state) log_idv_verification_submitted_event( success: false, - failure_reason: { idv_verification: [:timeout] }, ) end end @@ -192,14 +191,18 @@ def async_state_done(current_async_state) extra: { address_edited: !!idv_session.address_edited, address_line2_present: !pii[:address2].blank?, - pii_like_keypaths: [[:errors, :ssn], [:response_body, :first_name], - [:same_address_as_id], - [:state_id, :state_id_jurisdiction]], + pii_like_keypaths: [ + [:errors, :ssn], + [:proofing_results, :context, :stages, :resolution, :errors, :ssn], + [:proofing_results, :context, :stages, :residential_address, :errors, :ssn], + [:proofing_results, :context, :stages, :threatmetrix, :response_body, :first_name], + [:same_address_as_id], + [:proofing_results, :context, :stages, :state_id, :state_id_jurisdiction], + ], }, ) log_idv_verification_submitted_event( success: form_response.success?, - failure_reason: irs_attempts_api_tracker.parse_failure_reason(form_response), ) form_response.extra[:ssn_is_unique] = DuplicateSsnFinder.new( @@ -292,7 +295,7 @@ def idv_result_to_form_response( ) end - def log_idv_verification_submitted_event(success: false, failure_reason: nil) + def log_idv_verification_submitted_event(success: false) pii_from_doc = pii || {} irs_attempts_api_tracker.idv_verification_submitted( success: success, @@ -305,7 +308,6 @@ def log_idv_verification_submitted_event(success: false, failure_reason: nil) date_of_birth: pii_from_doc[:dob], address: pii_from_doc[:address1], ssn: idv_session.ssn, - failure_reason: failure_reason, ) end diff --git a/app/controllers/concerns/idv_step_concern.rb b/app/controllers/concerns/idv_step_concern.rb index 3ace1189618..0635ca4c020 100644 --- a/app/controllers/concerns/idv_step_concern.rb +++ b/app/controllers/concerns/idv_step_concern.rb @@ -110,7 +110,10 @@ def confirm_address_step_complete def extra_analytics_properties extra = { - pii_like_keypaths: [[:same_address_as_id], [:state_id, :state_id_jurisdiction]], + pii_like_keypaths: [ + [:same_address_as_id], + [:proofing_results, :context, :stages, :state_id, :state_id_jurisdiction], + ], } unless flow_session.dig(:pii_from_user, :same_address_as_id).nil? diff --git a/app/controllers/concerns/unconfirmed_user_concern.rb b/app/controllers/concerns/unconfirmed_user_concern.rb index 5742e67cff1..fff0089c3d0 100644 --- a/app/controllers/concerns/unconfirmed_user_concern.rb +++ b/app/controllers/concerns/unconfirmed_user_concern.rb @@ -28,7 +28,6 @@ def track_user_already_confirmed_event irs_attempts_api_tracker.user_registration_email_confirmation( email: @email_address.email, success: false, - failure_reason: { email: [:already_confirmed] }, ) end @@ -39,7 +38,6 @@ def stop_if_invalid_token irs_attempts_api_tracker.user_registration_email_confirmation( email: @email_address&.email, success: false, - failure_reason: irs_attempts_api_tracker.parse_failure_reason(result), ) process_unsuccessful_confirmation end diff --git a/app/controllers/frontend_log_controller.rb b/app/controllers/frontend_log_controller.rb index 430ac2d05ba..3e34430f7e2 100644 --- a/app/controllers/frontend_log_controller.rb +++ b/app/controllers/frontend_log_controller.rb @@ -10,7 +10,7 @@ class FrontendLogController < ApplicationController # Please try to keep this list alphabetical as well! # rubocop:disable Layout/LineLength - EVENT_MAP = { + LEGACY_EVENT_MAP = { 'Frontend Error' => FrontendErrorLogger.method(:track_error), 'IdV: Acuant SDK loaded' => :idv_acuant_sdk_loaded, 'IdV: back image added' => :idv_back_image_added, @@ -46,6 +46,12 @@ class FrontendLogController < ApplicationController }.freeze # rubocop:enable Layout/LineLength + ALLOWED_EVENTS = %i[ + phone_input_country_changed + ].freeze + + EVENT_MAP = ALLOWED_EVENTS.index_by(&:to_s).merge(LEGACY_EVENT_MAP).freeze + def create result = frontend_logger.track_event(log_params[:event], log_params[:payload].to_h) diff --git a/app/controllers/idv/by_mail/enter_code_controller.rb b/app/controllers/idv/by_mail/enter_code_controller.rb index 5b7b9ecac84..d5061a69468 100644 --- a/app/controllers/idv/by_mail/enter_code_controller.rb +++ b/app/controllers/idv/by_mail/enter_code_controller.rb @@ -58,7 +58,6 @@ def create analytics.idv_verify_by_mail_enter_code_submitted(**result.to_h) irs_attempts_api_tracker.idv_gpo_verification_submitted( success: result.success?, - failure_reason: irs_attempts_api_tracker.parse_failure_reason(result), ) if !result.success? diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index 72d254e14dd..be83c31b68d 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -54,18 +54,15 @@ def handle_phone_submission telephony_result = send_link telephony_form_response = build_telephony_form_response(telephony_result) - failure_reason = nil if !telephony_result.success? - failure_reason = { telephony: [telephony_result.error.class.name.demodulize] } failure(telephony_form_response.errors[:message]) end irs_attempts_api_tracker.idv_phone_upload_link_sent( success: telephony_result.success?, phone_number: formatted_destination_phone, - failure_reason: failure_reason, ) - if !failure_reason + if telephony_result.success? redirect_to idv_link_sent_url else redirect_to idv_hybrid_handoff_url diff --git a/app/controllers/idv/otp_verification_controller.rb b/app/controllers/idv/otp_verification_controller.rb index 2e34cf10173..f5327b95876 100644 --- a/app/controllers/idv/otp_verification_controller.rb +++ b/app/controllers/idv/otp_verification_controller.rb @@ -20,14 +20,9 @@ def update result = phone_confirmation_otp_verification_form.submit(code: params[:code]) analytics.idv_phone_confirmation_otp_submitted(**result.to_h) - parsed_failure_reason = - (result.extra.slice(:code_expired) if result.extra[:code_expired]) || - (result.extra.slice(:code_matches) if !result.success? && !result.extra[:code_matches]) || - {} irs_attempts_api_tracker.idv_phone_otp_submitted( success: result.success?, phone_number: idv_session.user_phone_confirmation_session.phone, - failure_reason: parsed_failure_reason, ) if result.success? diff --git a/app/controllers/idv/phone_controller.rb b/app/controllers/idv/phone_controller.rb index 672a8aa2538..811661fd035 100644 --- a/app/controllers/idv/phone_controller.rb +++ b/app/controllers/idv/phone_controller.rb @@ -48,7 +48,6 @@ def create irs_attempts_api_tracker.idv_phone_submitted( success: result.success?, phone_number: step_params[:phone], - failure_reason: irs_attempts_api_tracker.parse_failure_reason(result), ) if result.success? submit_proofing_attempt @@ -96,7 +95,6 @@ def send_phone_confirmation_otp_and_handle_result phone_number: @idv_phone, success: result.success?, otp_delivery_method: idv_session.previous_phone_step_params[:otp_delivery_preference], - failure_reason: result.success? ? {} : otp_sent_tracker_error(result), ) if result.success? redirect_to idv_otp_verification_url diff --git a/app/controllers/sign_up/email_confirmations_controller.rb b/app/controllers/sign_up/email_confirmations_controller.rb index 41dca8a0de0..5123e10dabc 100644 --- a/app/controllers/sign_up/email_confirmations_controller.rb +++ b/app/controllers/sign_up/email_confirmations_controller.rb @@ -26,7 +26,6 @@ def process_successful_confirmation irs_attempts_api_tracker.user_registration_email_confirmation( email: @email_address&.email, success: true, - failure_reason: nil, ) redirect_to sign_up_enter_password_url(confirmation_token: @confirmation_token) end diff --git a/app/controllers/sign_up/passwords_controller.rb b/app/controllers/sign_up/passwords_controller.rb index 6db85b2effe..5fc4a7d1aa4 100644 --- a/app/controllers/sign_up/passwords_controller.rb +++ b/app/controllers/sign_up/passwords_controller.rb @@ -39,12 +39,9 @@ def render_page end def track_analytics(result) - failure_reason = irs_attempts_api_tracker.parse_failure_reason(result) - analytics.password_creation(**result.to_h) irs_attempts_api_tracker.user_registration_password_submitted( success: result.success?, - failure_reason: failure_reason, ) end diff --git a/app/controllers/sign_up/registrations_controller.rb b/app/controllers/sign_up/registrations_controller.rb index d228d97bde0..25d47ac5998 100644 --- a/app/controllers/sign_up/registrations_controller.rb +++ b/app/controllers/sign_up/registrations_controller.rb @@ -30,7 +30,6 @@ def create irs_attempts_api_tracker.user_registration_email_submitted( email: permitted_params[:email], success: result.success?, - failure_reason: irs_attempts_api_tracker.parse_failure_reason(result), ) if result.success? diff --git a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb index 83dfd1d788a..9765ee21eef 100644 --- a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb +++ b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb @@ -45,7 +45,7 @@ def handle_result(result) if result.success? _event, disavowal_token = create_user_event_with_disavowal(:personal_key_used) alert_user_about_personal_key_sign_in(disavowal_token) - generate_new_personal_key_for_verified_users_otherwise_retire_the_key_and_ensure_two_mfa + remove_personal_key handle_valid_otp else handle_invalid_otp(context: context, type: 'personal_key') @@ -57,16 +57,6 @@ def alert_user_about_personal_key_sign_in(disavowal_token) analytics.personal_key_alert_about_sign_in(**response.to_h) end - def generate_new_personal_key_for_verified_users_otherwise_retire_the_key_and_ensure_two_mfa - if password_reset_profile.present? - re_encrypt_profile_recovery_pii - elsif current_user.identity_verified? - user_session[:personal_key] = PersonalKeyGenerator.new(current_user).create - else - remove_personal_key - end - end - def remove_personal_key # for now we will regenerate a key and not show it to them so retire personal key page shows current_user.personal_key = PersonalKeyGenerator.new(current_user).create @@ -74,28 +64,10 @@ def remove_personal_key user_session.delete(:personal_key) end - def re_encrypt_profile_recovery_pii - analytics.personal_key_reactivation_sign_in - Pii::ReEncryptor.new(pii: pii, profile: password_reset_profile).perform - user_session[:personal_key] = password_reset_profile.personal_key - end - - def password_reset_profile - @password_reset_profile ||= current_user.password_reset_profile - end - - def pii - @pii ||= password_reset_profile.recover_pii(normalized_personal_key) - end - def personal_key_param params[:personal_key_form][:personal_key] end - def normalized_personal_key - @personal_key_form.personal_key - end - def handle_valid_otp handle_valid_verification_for_authentication_context( auth_method: TwoFactorAuthenticatable::AuthMethod::PERSONAL_KEY, diff --git a/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb b/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb index c17654e382f..52555750d54 100644 --- a/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb +++ b/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb @@ -33,7 +33,6 @@ def process_token irs_attempts_api_tracker.mfa_login_piv_cac( success: result.success?, subject_dn: piv_cac_verification_form.x509_dn, - failure_reason: irs_attempts_api_tracker.parse_failure_reason(result), ) if result.success? handle_valid_piv_cac diff --git a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb index 0edb08b8fb1..28ba92782ba 100644 --- a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb +++ b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb @@ -36,7 +36,7 @@ def handle_webauthn_result(result) if result.success? handle_valid_webauthn else - handle_invalid_webauthn + handle_invalid_webauthn(result) end end @@ -54,24 +54,12 @@ def handle_valid_webauthn redirect_to after_sign_in_path_for(current_user) end - def handle_invalid_webauthn + def handle_invalid_webauthn(result) + flash[:error] = result.first_error_message + if platform_authenticator? - flash[:error] = t( - 'two_factor_authentication.webauthn_error.try_again', - link: view_context.link_to( - t('two_factor_authentication.webauthn_error.additional_methods_link'), - login_two_factor_options_path, - ), - ) redirect_to login_two_factor_webauthn_url(platform: 'true') else - flash[:error] = t( - 'two_factor_authentication.webauthn_error.connect_html', - link_html: view_context.link_to( - t('two_factor_authentication.webauthn_error.additional_methods_link'), - login_two_factor_options_path, - ), - ) redirect_to login_two_factor_webauthn_url end end @@ -124,6 +112,8 @@ def analytics_properties def form @form ||= WebauthnVerificationForm.new( user: current_user, + platform_authenticator: platform_authenticator?, + url_options:, challenge: user_session[:webauthn_challenge], protocol: request.protocol, authenticator_data: params[:authenticator_data], diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb index 611c42f8b39..15cfcfcca77 100644 --- a/app/controllers/users/passwords_controller.rb +++ b/app/controllers/users/passwords_controller.rb @@ -22,7 +22,6 @@ def update analytics.password_changed(**result.to_h) irs_attempts_api_tracker.logged_in_password_change( success: result.success?, - failure_reason: irs_attempts_api_tracker.parse_failure_reason(result), ) if result.success? diff --git a/app/controllers/users/piv_cac_authentication_setup_controller.rb b/app/controllers/users/piv_cac_authentication_setup_controller.rb index 73cef8934b9..6229003e160 100644 --- a/app/controllers/users/piv_cac_authentication_setup_controller.rb +++ b/app/controllers/users/piv_cac_authentication_setup_controller.rb @@ -88,7 +88,6 @@ def process_piv_cac_setup irs_attempts_api_tracker.mfa_enroll_piv_cac( success: result.success?, subject_dn: user_piv_cac_form.x509_dn, - failure_reason: irs_attempts_api_tracker.parse_failure_reason(result), ) if result.success? process_valid_submission diff --git a/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb b/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb index eca3141fbcc..ebea1debf03 100644 --- a/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb +++ b/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb @@ -43,7 +43,6 @@ def process_piv_cac_setup irs_attempts_api_tracker.mfa_enroll_piv_cac( success: result.success?, subject_dn: user_piv_cac_form.x509_dn, - failure_reason: irs_attempts_api_tracker.parse_failure_reason(result), ) if result.success? process_valid_submission diff --git a/app/controllers/users/reset_passwords_controller.rb b/app/controllers/users/reset_passwords_controller.rb index 1738b3e00d2..8ea9158e821 100644 --- a/app/controllers/users/reset_passwords_controller.rb +++ b/app/controllers/users/reset_passwords_controller.rb @@ -31,7 +31,6 @@ def edit analytics.password_reset_token(**result.to_h) irs_attempts_api_tracker.forgot_password_email_confirmed( success: result.success?, - failure_reason: irs_attempts_api_tracker.parse_failure_reason(result), ) if result.success? @reset_password_form = ResetPasswordForm.new(build_user) @@ -52,7 +51,6 @@ def update analytics.password_reset_password(**result.to_h) irs_attempts_api_tracker.forgot_password_new_password_submitted( success: result.success?, - failure_reason: irs_attempts_api_tracker.parse_failure_reason(result), ) if result.success? @@ -117,7 +115,6 @@ def create_account_if_email_not_found irs_attempts_api_tracker.user_registration_email_submitted( email: email, success: result.success?, - failure_reason: irs_attempts_api_tracker.parse_failure_reason(result), ) create_user_event(:account_created, user) end diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb index 14f823d11c3..d3b1602f3f1 100644 --- a/app/controllers/users/two_factor_authentication_controller.rb +++ b/app/controllers/users/two_factor_authentication_controller.rb @@ -232,7 +232,6 @@ def track_events(otp_delivery_preference:, otp_delivery_selection_result:) reauthentication: true, phone_number: parsed_phone.e164, otp_delivery_method: otp_delivery_preference, - failure_reason: irs_attempts_api_tracker.parse_failure_reason(@telephony_result), ) elsif UserSessionContext.authentication_or_reauthentication_context?(context) irs_attempts_api_tracker.mfa_login_phone_otp_sent( @@ -240,7 +239,6 @@ def track_events(otp_delivery_preference:, otp_delivery_selection_result:) reauthentication: false, phone_number: parsed_phone.e164, otp_delivery_method: otp_delivery_preference, - failure_reason: irs_attempts_api_tracker.parse_failure_reason(@telephony_result), ) elsif UserSessionContext.confirmation_context?(context) irs_attempts_api_tracker.mfa_enroll_phone_otp_sent( diff --git a/app/controllers/users/verify_personal_key_controller.rb b/app/controllers/users/verify_personal_key_controller.rb index 5c78899c73a..63406069873 100644 --- a/app/controllers/users/verify_personal_key_controller.rb +++ b/app/controllers/users/verify_personal_key_controller.rb @@ -29,11 +29,14 @@ def create analytics.personal_key_reactivation_submitted( **result.to_h, - pii_like_keypaths: [[:errors, :personal_key], [:error_details, :personal_key]], + pii_like_keypaths: [ + [:errors, :personal_key], + [:error_details, :personal_key], + [:error_details, :personal_key, :personal_key], + ], ) irs_attempts_api_tracker.personal_key_reactivation_submitted( success: result.success?, - failure_reason: irs_attempts_api_tracker.parse_failure_reason(result), ) if result.success? handle_success(decrypted_pii: personal_key_form.decrypted_pii) diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index 4f898eb9f89..d21ea2839ef 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -10,6 +10,7 @@ class WebauthnSetupController < ApplicationController before_action :apply_secure_headers_override before_action :set_webauthn_setup_presenter before_action :confirm_recently_authenticated_2fa + before_action :validate_existing_platform_authenticator helper_method :in_multi_mfa_selection_flow? @@ -108,6 +109,17 @@ def show_delete private + def validate_existing_platform_authenticator + if platform_authenticator? && in_account_creation_flow? && + current_user.webauthn_configurations.platform_authenticators.present? + redirect_to authentication_methods_setup_path + end + end + + def platform_authenticator? + params[:platform] == 'true' + end + def set_webauthn_setup_presenter @presenter = SetupPresenter.new( current_user: current_user, diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb index 6cde3e7eed7..b15b71c3af7 100644 --- a/app/forms/idv/api_image_upload_form.rb +++ b/app/forms/idv/api_image_upload_form.rb @@ -424,7 +424,6 @@ def track_event(response) last_name: pii_from_doc[:last_name], date_of_birth: pii_from_doc[:dob], address: pii_from_doc[:address1], - failure_reason: response.errors&.except(:hints)&.presence, ) end diff --git a/app/forms/idv/doc_pii_form.rb b/app/forms/idv/doc_pii_form.rb index 3616d2a8b94..e9eb97c01fc 100644 --- a/app/forms/idv/doc_pii_form.rb +++ b/app/forms/idv/doc_pii_form.rb @@ -49,10 +49,10 @@ def submit def self.pii_like_keypaths keypaths = [[:pii]] attrs = %i[name dob dob_min_age address1 state zipcode jurisdiction] - keypaths << attrs attrs.each do |k| keypaths << [:errors, k] keypaths << [:error_details, k] + keypaths << [:error_details, k, k] end keypaths end diff --git a/app/forms/update_user_password_form.rb b/app/forms/update_user_password_form.rb index 70a8e652aaa..9bda95c022e 100644 --- a/app/forms/update_user_password_form.rb +++ b/app/forms/update_user_password_form.rb @@ -24,7 +24,7 @@ def submit(params) def process_valid_submission update_user_password - encrypt_user_profile_if_active + encrypt_user_profiles end def update_user_password @@ -32,15 +32,18 @@ def update_user_password UpdateUser.new(user: user, attributes: attributes).call end - def encrypt_user_profile_if_active - active_profile = user.active_profile - return if active_profile.blank? + def encrypt_user_profiles + return if user.active_or_pending_profile.blank? - encryptor.call + encryptor.encrypt end def encryptor - @encryptor ||= ActiveProfileEncryptor.new(user, user_session, password) + @encryptor ||= UserProfilesEncryptor.new( + user: user, + user_session: user_session, + password: password, + ) end def extra_analytics_attributes diff --git a/app/forms/webauthn_verification_form.rb b/app/forms/webauthn_verification_form.rb index fa51beca58a..0ce31ead056 100644 --- a/app/forms/webauthn_verification_form.rb +++ b/app/forms/webauthn_verification_form.rb @@ -1,19 +1,29 @@ # The WebauthnVerificationForm class is responsible for validating webauthn verification input class WebauthnVerificationForm include ActiveModel::Model + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::TranslationHelper + include Rails.application.routes.url_helpers - validates :user, presence: true - validates :challenge, presence: true - validates :authenticator_data, presence: true - validates :client_data_json, presence: true - validates :signature, presence: true - validates :webauthn_configuration, presence: true + validates :challenge, + :authenticator_data, + :client_data_json, + :signature, + :webauthn_configuration, + presence: { message: proc { |object| object.instance_eval { generic_error_message } } } + validates :webauthn_error, + absence: { message: proc { |object| object.instance_eval { generic_error_message } } } validate :validate_assertion_response - validate :validate_webauthn_error + + attr_reader :url_options, :platform_authenticator + + alias_method :platform_authenticator?, :platform_authenticator def initialize( + user:, + platform_authenticator:, + url_options:, protocol:, - user: nil, challenge: nil, authenticator_data: nil, client_data_json: nil, @@ -22,6 +32,9 @@ def initialize( webauthn_error: nil ) @user = user + @platform_authenticator = platform_authenticator + @url_options = url_options + @protocol = protocol @challenge = challenge @protocol = protocol @authenticator_data = authenticator_data @@ -43,7 +56,7 @@ def submit def webauthn_configuration return @webauthn_configuration if defined?(@webauthn_configuration) - @webauthn_configuration = user&.webauthn_configurations&.find_by(credential_id: credential_id) + @webauthn_configuration = user.webauthn_configurations.find_by(credential_id: credential_id) end # this gives us a hook to override the domain embedded in the attestation test object @@ -64,12 +77,7 @@ def self.domain_name def validate_assertion_response return if webauthn_error.present? || webauthn_configuration.blank? || valid_assertion_response? - errors.add(:authenticator_data, 'invalid_authenticator_data', type: :invalid_authenticator_data) - end - - def validate_webauthn_error - return if webauthn_error.blank? - errors.add(:webauthn_error, webauthn_error, type: :webauthn_error) + errors.add(:authenticator_data, :invalid_authenticator_data, message: generic_error_message) end def valid_assertion_response? @@ -99,6 +107,26 @@ def public_key webauthn_configuration&.credential_public_key end + def generic_error_message + if platform_authenticator? + t( + 'two_factor_authentication.webauthn_error.try_again', + link: link_to( + t('two_factor_authentication.webauthn_error.additional_methods_link'), + login_two_factor_options_path, + ), + ) + else + t( + 'two_factor_authentication.webauthn_error.connect_html', + link_html: link_to( + t('two_factor_authentication.webauthn_error.additional_methods_link'), + login_two_factor_options_path, + ), + ) + end + end + def extra_analytics_attributes auth_method = if webauthn_configuration&.platform_authenticator 'webauthn_platform' @@ -109,6 +137,7 @@ def extra_analytics_attributes { multi_factor_auth_method: auth_method, webauthn_configuration_id: webauthn_configuration&.id, - } + frontend_error: webauthn_error.presence, + }.compact end end diff --git a/app/javascript/packages/analytics/README.md b/app/javascript/packages/analytics/README.md index fc184382e4e..752f2f17633 100644 --- a/app/javascript/packages/analytics/README.md +++ b/app/javascript/packages/analytics/README.md @@ -12,7 +12,7 @@ Track an event or error from your code using exported function members. import { trackEvent, trackError } from '@18f/identity-analytics'; button.addEventListener('click', () => { - trackEvent('Button clicked', { success: true }); + trackEvent('button_clicked', { success: true }); }); try { @@ -33,7 +33,7 @@ import '@18f/identity-analytics/click-observer-element'; The custom element will implement the analytics logging behavior, but all markup must already exist. ```html - + ``` diff --git a/app/javascript/packages/document-capture/components/acuant-capture.tsx b/app/javascript/packages/document-capture/components/acuant-capture.tsx index f0a71ac37be..21b36aae963 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.tsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.tsx @@ -62,7 +62,7 @@ interface ImageAnalyticsPayload { * Whether the Acuant SDK captured the image automatically, or using the tap to * capture functionality */ - acuantCaptureMode?: AcuantCaptureMode; + acuantCaptureMode?: AcuantCaptureMode | null; /** * Fingerprint of the image, base64 encoded SHA-256 digest @@ -376,7 +376,11 @@ function AcuantCapture( function getAddAttemptAnalyticsPayload< P extends ImageAnalyticsPayload | AcuantImageAnalyticsPayload, >(payload: P): P { - const enhancedPayload = { ...payload, attempt, acuantCaptureMode }; + const enhancedPayload = { + ...payload, + attempt, + acuantCaptureMode: payload.source === 'upload' ? null : acuantCaptureMode, + }; incrementAttempt(); return enhancedPayload; } @@ -387,6 +391,7 @@ function AcuantCapture( async function onUpload(nextValue: File | null) { let analyticsPayload: ImageAnalyticsPayload | undefined; let hasFailed = false; + if (nextValue) { const { width, height, fingerprint } = await getImageMetadata(nextValue); hasFailed = failedSubmissionImageFingerprints[name]?.includes(fingerprint); diff --git a/app/javascript/packages/normalize-yaml/CHANGELOG.md b/app/javascript/packages/normalize-yaml/CHANGELOG.md index 68d0e0d834e..e0bf3d9170f 100644 --- a/app/javascript/packages/normalize-yaml/CHANGELOG.md +++ b/app/javascript/packages/normalize-yaml/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased +## v2.0.0 ### Breaking Changes diff --git a/app/javascript/packages/normalize-yaml/package.json b/app/javascript/packages/normalize-yaml/package.json index 53afca1b3f6..b3127e7831f 100644 --- a/app/javascript/packages/normalize-yaml/package.json +++ b/app/javascript/packages/normalize-yaml/package.json @@ -2,7 +2,7 @@ "name": "@18f/identity-normalize-yaml", "private": false, "description": "Normalizes YAML files to ensure consistency and typographical quality", - "version": "1.0.0", + "version": "2.0.0", "type": "module", "main": "./index.js", "types": "./types/index.d.ts", @@ -34,7 +34,7 @@ }, "dependencies": { "smartquotes": "^2.3.2", - "yaml": "^2.3.1" + "yaml": "^2.3.4" }, "peerDependencies": { "prettier": ">=3" diff --git a/app/javascript/packages/phone-input/index.spec.ts b/app/javascript/packages/phone-input/index.spec.ts index 30102cb61f6..10d4c193a96 100644 --- a/app/javascript/packages/phone-input/index.spec.ts +++ b/app/javascript/packages/phone-input/index.spec.ts @@ -2,6 +2,7 @@ import { getByLabelText, getByRole, getAllByRole } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; import { computeAccessibleName } from 'dom-accessibility-api'; import type { SinonStub } from 'sinon'; +import * as analytics from '@18f/identity-analytics'; import { useSandbox } from '@18f/identity-test-helpers'; import { CAPTCHA_EVENT_NAME } from '@18f/identity-captcha-submit-button/captcha-submit-button-element'; @@ -31,6 +32,10 @@ describe('PhoneInput', () => { await import('./index'); }); + beforeEach(() => { + sandbox.stub(analytics, 'trackEvent'); + }); + function createAndConnectElement({ isUSSingleOption = false, isInternationalSingleOption = false, @@ -180,6 +185,30 @@ describe('PhoneInput', () => { expect(phoneNumber.value).to.equal('+1 071'); }); + it('tracks event on country change', async () => { + const input = createAndConnectElement(); + const iti = input.querySelector('.iti') as HTMLElement; + + const phoneNumber = getByLabelText(input, 'Phone number') as HTMLInputElement; + + await userEvent.type(phoneNumber, '+1306'); + expect(analytics.trackEvent).to.have.been.calledOnceWith('phone_input_country_changed', { + country_code: 'CA', + }); + + const dropdownButton = getByRole(iti, 'combobox', { name: 'Country code' }); + await userEvent.click(dropdownButton); + const usOption = getByRole(iti, 'option', { name: 'United States +1' }); + await userEvent.click(usOption); + expect(analytics.trackEvent).to.have.been.calledWith('phone_input_country_changed', { + country_code: 'US', + }); + + await userEvent.clear(phoneNumber); + await userEvent.type(phoneNumber, '+6'); + expect(analytics.trackEvent).to.have.callCount(2); + }); + it('renders as an accessible combobox', () => { const phoneInput = createAndConnectElement(); const comboboxes = getAllByRole(phoneInput, 'combobox'); diff --git a/app/javascript/packages/phone-input/index.ts b/app/javascript/packages/phone-input/index.ts index e5dd328c35c..a0260398316 100644 --- a/app/javascript/packages/phone-input/index.ts +++ b/app/javascript/packages/phone-input/index.ts @@ -5,6 +5,7 @@ import type { CountryCode } from 'libphonenumber-js'; import type { Plugin as IntlTelInputPlugin, Options } from 'intl-tel-input'; import { replaceVariables } from '@18f/identity-i18n'; import { CAPTCHA_EVENT_NAME } from '@18f/identity-captcha-submit-button/captcha-submit-button-element'; +import { trackEvent } from '@18f/identity-analytics'; interface PhoneInputStrings { country_code_label: string; @@ -61,6 +62,7 @@ export class PhoneInputElement extends HTMLElement { this.initializeIntlTelInput(); this.textInput.addEventListener('countrychange', () => this.syncCountryToCodeInput()); + this.textInput.addEventListener('countrychange', () => this.trackCountryChangeEvent()); this.textInput.addEventListener('input', () => this.validate()); this.codeInput.addEventListener('change', () => this.formatTextInput()); this.codeInput.addEventListener('change', () => this.validate()); @@ -122,13 +124,23 @@ export class PhoneInputElement extends HTMLElement { } } + /** + * Logs an event when the country code has been changed. + */ + trackCountryChangeEvent() { + const countryCode = this.getSelectedCountryCode(); + if (countryCode) { + trackEvent('phone_input_country_changed', { country_code: countryCode }); + } + } + /** * Mirrors country change to the hidden select field, which holds the value for form submission. */ syncCountryToCodeInput({ fireChangeEvent = true }: { fireChangeEvent?: boolean } = {}) { - const country = this.iti.getSelectedCountryData(); - if (country.iso2 && this.codeInput) { - this.codeInput.value = country.iso2.toUpperCase(); + const countryCode = this.getSelectedCountryCode(); + if (countryCode) { + this.codeInput.value = countryCode; if (this.hasDropdown) { // Move value text from title attribute to the flag's hidden text element. // See: https://github.com/jackocnr/intl-tel-input/blob/d54b127/src/js/intlTelInput.js#L1191-L1197 @@ -257,16 +269,20 @@ export class PhoneInputElement extends HTMLElement { } handleCaptchaChallenge = (event: Event) => { - const { iso2 = 'us' } = this.iti.getSelectedCountryData(); + const countryCode = this.getSelectedCountryCode(); const isExempt = typeof this.captchaExemptCountries === 'boolean' ? this.captchaExemptCountries - : this.captchaExemptCountries.includes(iso2.toUpperCase()); + : !countryCode || this.captchaExemptCountries.includes(countryCode); if (isExempt) { event.preventDefault(); } }; + + getSelectedCountryCode(): string | undefined { + return (this.iti.getSelectedCountryData().iso2 as string | undefined)?.toUpperCase(); + } } declare global { diff --git a/app/javascript/packages/rails-i18n-webpack-plugin/package.json b/app/javascript/packages/rails-i18n-webpack-plugin/package.json index 3bd84c9d1b4..948ecbe20af 100644 --- a/app/javascript/packages/rails-i18n-webpack-plugin/package.json +++ b/app/javascript/packages/rails-i18n-webpack-plugin/package.json @@ -5,7 +5,7 @@ "main": "rails-i18n-webpack-plugin.js", "dependencies": { "webpack-sources": "^2.3.0", - "yaml": "^2.3.1" + "yaml": "^2.3.4" }, "peerDependencies": { "sinon": "*", diff --git a/app/javascript/packages/stylelint-config/CHANGELOG.md b/app/javascript/packages/stylelint-config/CHANGELOG.md index 7b1f6d2bcf6..0f30db00501 100644 --- a/app/javascript/packages/stylelint-config/CHANGELOG.md +++ b/app/javascript/packages/stylelint-config/CHANGELOG.md @@ -1,11 +1,16 @@ -## Unreleased +## 3.0.0 ### Breaking Changes - Breaking changes included in updated dependencies: - [`stylelint-prettier`](https://github.com/prettier/stylelint-prettier/blob/main/CHANGELOG.md): - - Minimum supported `prettier` version is now `v3.0.0`. - - Minimum supported `stylelint` version is now `v15.8.0`. + - Dropped support for `prettier` versions below `v3.0.0`. + - Dropped support for `stylelint` versions below `v15.8.0`. + - [`stylelint-config-recommended-scss`](https://github.com/stylelint-scss/stylelint-config-recommended-scss/blob/master/CHANGELOG.md) + - Dropped support for `stylelint` versions below `v15.10.0`. + - [`stylelint-config-recommended`](https://github.com/stylelint/stylelint-config-recommended/blob/main/CHANGELOG.md) + - Changed defaults may identify new issues in your existing code: + - [`stylelint-config-recommended@13.0.0`](https://github.com/stylelint/stylelint-config-recommended/releases/tag/13.0.0) added `media-query-no-invalid` ## 2.0.0 diff --git a/app/javascript/packages/stylelint-config/package.json b/app/javascript/packages/stylelint-config/package.json index 195423bebcd..c8a634b13c4 100644 --- a/app/javascript/packages/stylelint-config/package.json +++ b/app/javascript/packages/stylelint-config/package.json @@ -1,6 +1,6 @@ { "name": "@18f/identity-stylelint-config", - "version": "2.0.0", + "version": "3.0.0", "private": false, "description": "Stylelint shareable configuration for Login.gov CSS/SASS standards", "repository": { @@ -17,11 +17,11 @@ }, "homepage": "https://github.com/18f/identity-idp", "dependencies": { - "stylelint-config-recommended-scss": "^10.0.0", + "stylelint-config-recommended-scss": "^13.1.0", "stylelint-prettier": "^4.0.2" }, "peerDependencies": { - "prettier": "*", - "stylelint": "^15.0.0" + "prettier": ">=3.0.0", + "stylelint": ">=15.10.0" } } diff --git a/app/javascript/packs/puerto-rico-guidance.ts b/app/javascript/packs/state-guidance.ts similarity index 51% rename from app/javascript/packs/puerto-rico-guidance.ts rename to app/javascript/packs/state-guidance.ts index 117bb3d5d15..a317fd668da 100644 --- a/app/javascript/packs/puerto-rico-guidance.ts +++ b/app/javascript/packs/state-guidance.ts @@ -1,3 +1,16 @@ +export function showOrHideJurisdictionExtras(jurisdictionCode) { + const hasJurisdictionSpecificHint = + jurisdictionCode && + document.querySelectorAll(`.jurisdiction-extras [data-state=${jurisdictionCode}]`).length > 0; + + document.querySelectorAll(`.jurisdiction-extras [data-state]`).forEach((element) => { + const shouldShow = + element.dataset.state === jurisdictionCode || + (!hasJurisdictionSpecificHint && element.dataset.state === 'default'); + element.classList.toggle('display-none', !shouldShow); + }); +} + export function showOrHidePuertoRicoExtras(forStateCode) { const isPuertoRico = forStateCode === 'PR'; @@ -15,6 +28,7 @@ function onStateSelectionChange() { document.getElementById('idv_form_state') ); showOrHidePuertoRicoExtras(stateSelector?.value); + showOrHideJurisdictionExtras(stateSelector?.value); } function onIdentityDocStateSelection() { @@ -29,9 +43,22 @@ function onIdentityDocStateSelection() { }); } +function onIdentityDocJurisdictionSelection() { + const stateSelectors = document.querySelectorAll('.jurisdiction-state-selector'); + stateSelectors.forEach((stateSelector) => { + if (stateSelector instanceof HTMLSelectElement) { + stateSelector.addEventListener('change', () => + showOrHideJurisdictionExtras(stateSelector.value), + ); + showOrHideJurisdictionExtras(stateSelector.value); + } + }); +} + document.getElementById('idv_form_state')?.addEventListener('change', onStateSelectionChange); document.addEventListener('DOMContentLoaded', () => { onStateSelectionChange(); onIdentityDocStateSelection(); + onIdentityDocJurisdictionSelection(); }); diff --git a/app/javascript/packs/webauthn-setup.ts b/app/javascript/packs/webauthn-setup.ts index 7195ea4093c..614335d8d23 100644 --- a/app/javascript/packs/webauthn-setup.ts +++ b/app/javascript/packs/webauthn-setup.ts @@ -64,9 +64,8 @@ function webauthn() { (document.getElementById('client_data_json') as HTMLInputElement).value = result.clientDataJSON; if (result.authenticatorDataFlagsValue) { - ( - document.getElementById('authenticator_data_value') as HTMLInputElement - ).value = `${result.authenticatorDataFlagsValue}`; + (document.getElementById('authenticator_data_value') as HTMLInputElement).value = + `${result.authenticatorDataFlagsValue}`; } if (result.transports) { (document.getElementById('transports') as HTMLInputElement).value = diff --git a/app/models/phone_configuration.rb b/app/models/phone_configuration.rb index 15311c0ffef..43c2e5ccb22 100644 --- a/app/models/phone_configuration.rb +++ b/app/models/phone_configuration.rb @@ -22,11 +22,13 @@ def selection_presenters capabilities = PhoneNumberCapabilities.new(phone, phone_confirmed: !!confirmed_at?) if capabilities.supports_sms? - options << TwoFactorAuthentication::SmsSelectionPresenter.new(user:, configuration: self) + options << TwoFactorAuthentication::SignInPhoneSelectionPresenter. + new(user:, configuration: self, delivery_method: :sms) end if capabilities.supports_voice? - options << TwoFactorAuthentication::VoiceSelectionPresenter.new(user:, configuration: self) + options << TwoFactorAuthentication::SignInPhoneSelectionPresenter. + new(user:, configuration: self, delivery_method: :voice) end options diff --git a/app/models/user.rb b/app/models/user.rb index e4c4f58391b..1c17fce179d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -122,6 +122,10 @@ def suspend! OutOfBandSessionAccessor.new(unique_session_id).destroy if unique_session_id update!(suspended_at: Time.zone.now, unique_session_id: nil) analytics.user_suspended(success: true) + + event = PushNotification::AccountDisabledEvent.new(user: self) + PushNotification::HttpPush.deliver(event) + email_addresses.map do |email_address| SuspendedEmail.create_from_email_address!(email_address) end @@ -134,6 +138,10 @@ def reinstate! end update!(reinstated_at: Time.zone.now) analytics.user_reinstated(success: true) + + event = PushNotification::AccountEnabledEvent.new(user: self) + PushNotification::HttpPush.deliver(event) + email_addresses.map do |email_address| SuspendedEmail.find_with_email(email_address.email)&.destroy end diff --git a/app/presenters/two_factor_authentication/selection_presenter.rb b/app/presenters/two_factor_authentication/selection_presenter.rb deleted file mode 100644 index 3a52380e482..00000000000 --- a/app/presenters/two_factor_authentication/selection_presenter.rb +++ /dev/null @@ -1,131 +0,0 @@ -module TwoFactorAuthentication - class SelectionPresenter - include ActionView::Helpers::TranslationHelper - - attr_reader :configuration, :user - - def initialize(user:, configuration: nil) - @user = user - @configuration = configuration - end - - def render_in(view_context, &block) - view_context.capture(&block) - end - - def type - method.to_s - end - - def label - if @configuration.present? - login_label(method.to_s) - else - setup_label(method.to_s) - end - end - - def info - if @configuration.present? - login_info(method.to_s) - else - setup_info(method.to_s) - end - end - - def mfa_configuration_count - 0 - end - - def mfa_configuration_description - return '' if mfa_configuration_count == 0 - if single_configuration_only? - t('two_factor_authentication.two_factor_choice_options.no_count_configuration_added') - else - t( - 'two_factor_authentication.two_factor_choice_options.configurations_added', - count: mfa_configuration_count, - ) - end - end - - def mfa_added_label - if single_configuration_only? - '' - else - "(#{mfa_configuration_description})" - end - end - - def single_configuration_only? - false - end - - def disabled? - configuration.blank? && single_configuration_only? && mfa_configuration_count > 0 - end - - private - - def login_label(type) - case type - when 'sms' - t('two_factor_authentication.login_options.sms') - when 'voice' - t('two_factor_authentication.login_options.voice') - else - raise "Unsupported login method: #{type}" - end - end - - def setup_label(type) - case type - when 'phone' - t('two_factor_authentication.two_factor_choice_options.phone') - when 'sms' - t('two_factor_authentication.two_factor_choice_options.sms') - when 'voice' - t('two_factor_authentication.two_factor_choice_options.voice') - else - raise "Unsupported setup method: #{type}" - end - end - - def login_info(type) - case type - when 'auth_app' - t('two_factor_authentication.login_options.auth_app_info') - when 'backup_code' - t('two_factor_authentication.login_options.backup_code_info') - when 'piv_cac' - t('two_factor_authentication.login_options.piv_cac_info') - when 'webauthn' - t('two_factor_authentication.login_options.webauthn_info') - when 'webauthn_platform' - t('two_factor_authentication.login_options.webauthn_platform_info', app_name: APP_NAME) - else - raise "Unsupported login method: #{type}" - end - end - - def setup_info(type) - case type - when 'auth_app' - t('two_factor_authentication.two_factor_choice_options.auth_app_info') - when 'backup_code' - t('two_factor_authentication.two_factor_choice_options.backup_code_info') - when 'piv_cac' - t('two_factor_authentication.two_factor_choice_options.piv_cac_info') - when 'webauthn' - t('two_factor_authentication.two_factor_choice_options.webauthn_info') - when 'webauthn_platform' - t( - 'two_factor_authentication.two_factor_choice_options.webauthn_platform_info', - app_name: APP_NAME, - ) - else - raise "Unsupported setup method: #{type}" - end - end - end -end diff --git a/app/presenters/two_factor_authentication/set_up_auth_app_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_auth_app_selection_presenter.rb index 0763712895d..d2226caba96 100644 --- a/app/presenters/two_factor_authentication/set_up_auth_app_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_auth_app_selection_presenter.rb @@ -1,6 +1,6 @@ module TwoFactorAuthentication class SetUpAuthAppSelectionPresenter < SetUpSelectionPresenter - def method + def type :auth_app end diff --git a/app/presenters/two_factor_authentication/set_up_backup_code_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_backup_code_selection_presenter.rb index 890ad3248a9..cf4046ffc3a 100644 --- a/app/presenters/two_factor_authentication/set_up_backup_code_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_backup_code_selection_presenter.rb @@ -1,6 +1,6 @@ module TwoFactorAuthentication class SetUpBackupCodeSelectionPresenter < SetUpSelectionPresenter - def method + def type :backup_code end diff --git a/app/presenters/two_factor_authentication/phone_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_phone_selection_presenter.rb similarity index 60% rename from app/presenters/two_factor_authentication/phone_selection_presenter.rb rename to app/presenters/two_factor_authentication/set_up_phone_selection_presenter.rb index 398fb43d38c..111270c8946 100644 --- a/app/presenters/two_factor_authentication/phone_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_phone_selection_presenter.rb @@ -1,15 +1,11 @@ module TwoFactorAuthentication - class PhoneSelectionPresenter < SelectionPresenter - def method + class SetUpPhoneSelectionPresenter < SetUpSelectionPresenter + def type :phone end - def type - if MfaContext.new(configuration&.user).phone_configurations.many? - "#{super}_#{configuration.id}" - else - super - end + def label + t('two_factor_authentication.two_factor_choice_options.phone') end def info diff --git a/app/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter.rb index 4022165b49f..76bf98f2724 100644 --- a/app/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter.rb @@ -1,6 +1,6 @@ module TwoFactorAuthentication class SetUpPivCacSelectionPresenter < SetUpSelectionPresenter - def method + def type :piv_cac end diff --git a/app/presenters/two_factor_authentication/set_up_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_selection_presenter.rb index 0c4e798eb89..d118a0af06c 100644 --- a/app/presenters/two_factor_authentication/set_up_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_selection_presenter.rb @@ -13,15 +13,15 @@ def render_in(view_context, &block) end def type - method.to_s + raise NotImplementedError end def label - raise "Unsupported setup method: #{type}" + raise NotImplementedError end def info - raise "Unsupported setup method: #{type}" + raise NotImplementedError end def mfa_added_label diff --git a/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb index 278fb3b1ad9..0ed7f1158c5 100644 --- a/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb @@ -5,7 +5,7 @@ def initialize(user:, configuration: nil) @configuration = configuration end - def method + def type :webauthn_platform end diff --git a/app/presenters/two_factor_authentication/set_up_webauthn_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_webauthn_selection_presenter.rb index 8f95676d18f..268f594e07d 100644 --- a/app/presenters/two_factor_authentication/set_up_webauthn_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_webauthn_selection_presenter.rb @@ -1,6 +1,6 @@ module TwoFactorAuthentication class SetUpWebauthnSelectionPresenter < SetUpSelectionPresenter - def method + def type :webauthn end @@ -13,7 +13,7 @@ def label end def info - t('two_factor_authentication.login_options.webauthn_info') + t('two_factor_authentication.two_factor_choice_options.webauthn_info') end def mfa_configuration_count diff --git a/app/presenters/two_factor_authentication/sign_in_auth_app_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_auth_app_selection_presenter.rb index 59e268502ba..a3fc889ecdd 100644 --- a/app/presenters/two_factor_authentication/sign_in_auth_app_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/sign_in_auth_app_selection_presenter.rb @@ -1,6 +1,6 @@ module TwoFactorAuthentication class SignInAuthAppSelectionPresenter < SignInSelectionPresenter - def method + def type :auth_app end diff --git a/app/presenters/two_factor_authentication/sign_in_backup_code_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_backup_code_selection_presenter.rb index 40b65dcff3b..b9e315c4719 100644 --- a/app/presenters/two_factor_authentication/sign_in_backup_code_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/sign_in_backup_code_selection_presenter.rb @@ -1,6 +1,6 @@ module TwoFactorAuthentication class SignInBackupCodeSelectionPresenter < SignInSelectionPresenter - def method + def type :backup_code end diff --git a/app/presenters/two_factor_authentication/sign_in_personal_key_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_personal_key_selection_presenter.rb index 4a863f85540..b0fd6aeb722 100644 --- a/app/presenters/two_factor_authentication/sign_in_personal_key_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/sign_in_personal_key_selection_presenter.rb @@ -1,6 +1,6 @@ module TwoFactorAuthentication - class SignInPersonalKeySelectionPresenter < SelectionPresenter - def method + class SignInPersonalKeySelectionPresenter < SignInSelectionPresenter + def type :personal_key end diff --git a/app/presenters/two_factor_authentication/sign_in_phone_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_phone_selection_presenter.rb new file mode 100644 index 00000000000..f9c27d2409b --- /dev/null +++ b/app/presenters/two_factor_authentication/sign_in_phone_selection_presenter.rb @@ -0,0 +1,55 @@ +module TwoFactorAuthentication + class SignInPhoneSelectionPresenter < SignInSelectionPresenter + attr_reader :configuration, :user, :delivery_method + + def initialize(user:, configuration:, delivery_method:) + @user = user + @configuration = configuration + @delivery_method = delivery_method + end + + def type + if MfaContext.new(configuration&.user).phone_configurations.many? + "#{delivery_method}_#{configuration.id}".to_sym + else + delivery_method || :phone + end + end + + def label + if delivery_method == :sms + t('two_factor_authentication.login_options.sms') + elsif delivery_method == :voice + t('two_factor_authentication.login_options.voice') + end + end + + def info + case delivery_method + when :sms + t( + 'two_factor_authentication.login_options.sms_info_html', + phone: configuration.masked_phone, + ) + when :voice + t( + 'two_factor_authentication.login_options.voice_info_html', + phone: configuration.masked_phone, + ) + else + t('two_factor_authentication.two_factor_choice_options.phone_info') + end + end + + def disabled? + case delivery_method + when :sms + OutageStatus.new.vendor_outage?(:sms) + when :voice + OutageStatus.new.vendor_outage?(:voice) + else + OutageStatus.new.all_phone_vendor_outage? + end + end + end +end diff --git a/app/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.rb index a3f38c6044b..8b709c2c87b 100644 --- a/app/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.rb @@ -1,6 +1,6 @@ module TwoFactorAuthentication class SignInPivCacSelectionPresenter < SignInSelectionPresenter - def method + def type :piv_cac end diff --git a/app/presenters/two_factor_authentication/sign_in_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_selection_presenter.rb index fbffd1a0960..f954551b94a 100644 --- a/app/presenters/two_factor_authentication/sign_in_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/sign_in_selection_presenter.rb @@ -14,15 +14,15 @@ def render_in(view_context, &block) end def type - method.to_s + raise NotImplementedError end def label - raise "Unsupported login method: #{type}" + raise NotImplementedError end def info - raise "Unsupported login method: #{type}" + raise NotImplementedError end def disabled? diff --git a/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb index b1632c78998..85c6b710991 100644 --- a/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb @@ -1,6 +1,6 @@ module TwoFactorAuthentication - class SignInWebauthnPlatformSelectionPresenter < SelectionPresenter - def method + class SignInWebauthnPlatformSelectionPresenter < SignInSelectionPresenter + def type :webauthn_platform end diff --git a/app/presenters/two_factor_authentication/sign_in_webauthn_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_webauthn_selection_presenter.rb index 542ea6a1289..b38ed74d4d1 100644 --- a/app/presenters/two_factor_authentication/sign_in_webauthn_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/sign_in_webauthn_selection_presenter.rb @@ -1,6 +1,6 @@ module TwoFactorAuthentication class SignInWebauthnSelectionPresenter < SignInSelectionPresenter - def method + def type :webauthn end diff --git a/app/presenters/two_factor_authentication/sms_selection_presenter.rb b/app/presenters/two_factor_authentication/sms_selection_presenter.rb deleted file mode 100644 index 3226ae6adc3..00000000000 --- a/app/presenters/two_factor_authentication/sms_selection_presenter.rb +++ /dev/null @@ -1,22 +0,0 @@ -module TwoFactorAuthentication - class SmsSelectionPresenter < PhoneSelectionPresenter - def method - :sms - end - - def info - if configuration.present? - t( - 'two_factor_authentication.login_options.sms_info_html', - phone: configuration.masked_phone, - ) - else - super - end - end - - def disabled? - OutageStatus.new.vendor_outage?(:sms) - end - end -end diff --git a/app/presenters/two_factor_authentication/voice_selection_presenter.rb b/app/presenters/two_factor_authentication/voice_selection_presenter.rb deleted file mode 100644 index 369878c5e5d..00000000000 --- a/app/presenters/two_factor_authentication/voice_selection_presenter.rb +++ /dev/null @@ -1,22 +0,0 @@ -module TwoFactorAuthentication - class VoiceSelectionPresenter < PhoneSelectionPresenter - def method - :voice - end - - def info - if configuration.present? - t( - 'two_factor_authentication.login_options.voice_info_html', - phone: configuration.masked_phone, - ) - else - super - end - end - - def disabled? - OutageStatus.new.vendor_outage?(:voice) - end - end -end diff --git a/app/presenters/two_factor_options_presenter.rb b/app/presenters/two_factor_options_presenter.rb index 7466c3c8f3d..56a4567a328 100644 --- a/app/presenters/two_factor_options_presenter.rb +++ b/app/presenters/two_factor_options_presenter.rb @@ -34,7 +34,7 @@ def all_user_selected_options [ TwoFactorAuthentication::SetUpWebauthnPlatformSelectionPresenter.new(user: user), TwoFactorAuthentication::SetUpAuthAppSelectionPresenter.new(user: user), - TwoFactorAuthentication::PhoneSelectionPresenter.new(user: user), + TwoFactorAuthentication::SetUpPhoneSelectionPresenter.new(user: user), TwoFactorAuthentication::SetUpBackupCodeSelectionPresenter.new(user: user), TwoFactorAuthentication::SetUpWebauthnSelectionPresenter.new(user: user), TwoFactorAuthentication::SetUpPivCacSelectionPresenter.new(user: user), @@ -116,7 +116,7 @@ def phone_options if piv_cac_required? || phishing_resistant_only? || IdentityConfig.store.hide_phone_mfa_signup return [] else - [TwoFactorAuthentication::PhoneSelectionPresenter.new(user: user)] + [TwoFactorAuthentication::SetUpPhoneSelectionPresenter.new(user: user)] end end diff --git a/app/services/account_reset/track_irs_event.rb b/app/services/account_reset/track_irs_event.rb index 2081eab1d24..b858979091f 100644 --- a/app/services/account_reset/track_irs_event.rb +++ b/app/services/account_reset/track_irs_event.rb @@ -9,7 +9,6 @@ module AccountReset::TrackIrsEvent def track_irs_event irs_attempts_api_tracker.account_reset_account_deleted( success: success, - failure_reason: event_failure_reason.presence, ) end @@ -20,8 +19,4 @@ def irs_attempts_api_tracker def cookies request.cookie_jar end - - def event_failure_reason - errors.is_a?(ActiveModel::Errors) ? errors.messages.to_hash : errors - end end diff --git a/app/services/active_profile_encryptor.rb b/app/services/active_profile_encryptor.rb deleted file mode 100644 index 20b6b87ffa9..00000000000 --- a/app/services/active_profile_encryptor.rb +++ /dev/null @@ -1,22 +0,0 @@ -class ActiveProfileEncryptor - attr_reader :personal_key - - def initialize(user, user_session, password) - @user = user - @user_session = user_session - @password = password - end - - def call - active_profile = @user.active_profile - @personal_key = active_profile.encrypt_pii(current_pii, @password) - active_profile.save! - end - - private - - def current_pii - cacher = Pii::Cacher.new(@user, @user_session) - cacher.fetch - end -end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 89923d5a914..8bf937e0877 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -3123,6 +3123,7 @@ def logout_initiated( # @param [String] area_code # @param [String] country_code # @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164 + # @param [String] frontend_error Name of error that occurred in frontend during submission # Multi-Factor Authentication def multi_factor_auth( success:, @@ -3140,6 +3141,7 @@ def multi_factor_auth( area_code: nil, country_code: nil, phone_fingerprint: nil, + frontend_error: nil, **extra ) track_event( @@ -3159,6 +3161,7 @@ def multi_factor_auth( area_code: area_code, country_code: country_code, phone_fingerprint: phone_fingerprint, + frontend_error:, **extra, ) end @@ -3781,13 +3784,6 @@ def personal_key_reactivation track_event('Personal key reactivation: Account reactivated with personal key') end - # Account reactivated with personal key as MFA - def personal_key_reactivation_sign_in - track_event( - 'Personal key reactivation: Account reactivated with personal key as MFA', - ) - end - # @param [Boolean] success # @param [Hash] errors # @param [Hash] pii_like_keypaths @@ -3859,6 +3855,12 @@ def phone_deletion(success:, phone_configuration_id:, **extra) ) end + # @param [String] country_code The new selected country code + # User changes the selected country in the frontend phone input component + def phone_input_country_changed(country_code:, **extra) + track_event(:phone_input_country_changed, country_code:, **extra) + end + # @identity.idp.previous_event_name User Registration: piv cac disabled # @identity.idp.previous_event_name PIV CAC disabled # Tracks when user's piv cac is disabled diff --git a/app/services/doc_auth/acuant/issuer_types.rb b/app/services/doc_auth/acuant/issuer_types.rb new file mode 100644 index 00000000000..94d5ee77126 --- /dev/null +++ b/app/services/doc_auth/acuant/issuer_types.rb @@ -0,0 +1,31 @@ +module DocAuth + module Acuant + module IssuerTypes + IssuerType = Struct.new(:code, :name) + + UNKNOWN = IssuerType.new(0, 'Unknown').freeze + COUNTRY = IssuerType.new(1, 'Country').freeze + STATE_OR_PROVINCE = IssuerType.new(2, 'StateProvince').freeze + TRIBAL = IssuerType.new(3, 'Tribal').freeze + MUNICIPALITY = IssuerType.new(4, 'Municipality').freeze + BUSINESS = IssuerType.new(5, 'Business').freeze + OTHER = IssuerType.new(6, 'Other').freeze + + ALL = [ + UNKNOWN, + COUNTRY, + STATE_OR_PROVINCE, + TRIBAL, + MUNICIPALITY, + BUSINESS, + OTHER, + ].freeze + + BY_CODE = ALL.index_by(&:code).freeze + + def self.from_int(code) + BY_CODE[code] + end + end + end +end diff --git a/app/services/doc_auth/acuant/responses/get_results_response.rb b/app/services/doc_auth/acuant/responses/get_results_response.rb index 51c46e73602..1b32808bdcc 100644 --- a/app/services/doc_auth/acuant/responses/get_results_response.rb +++ b/app/services/doc_auth/acuant/responses/get_results_response.rb @@ -114,7 +114,7 @@ def classification_info return unless classification_details.present? classification_details.transform_values do |classification_detail| - classification_detail.slice( + detail = classification_detail.slice( 'ClassName', 'Issue', 'IssueType', @@ -122,8 +122,12 @@ def classification_info 'IssuerCode', 'IssuerName', 'CountryCode', + 'IssuerType', ) - end + # code to value + detail['IssuerType'] = DocAuth::Acuant::IssuerTypes.from_int(detail['IssuerType']).name + detail + end.deep_symbolize_keys end def successful_result? diff --git a/app/services/doc_auth/classification_concern.rb b/app/services/doc_auth/classification_concern.rb index 8d7e8622680..bbfb2b12ff9 100644 --- a/app/services/doc_auth/classification_concern.rb +++ b/app/services/doc_auth/classification_concern.rb @@ -7,11 +7,13 @@ def id_type_supported? info = classification_info return true if info.nil? type_ok = doc_side_class_ok?(info, 'Front') && doc_side_class_ok?(info, 'Back') + return false unless type_ok issuing_country_ok = doc_issuing_country_ok?( info, 'Front', ) && doc_issuing_country_ok?(info, 'Back') - type_ok && issuing_country_ok + return false unless issuing_country_ok + doc_issuer_type_ok?(info, :Front) && doc_issuer_type_ok?(info, :Back) end alias_method :doc_type_supported?, :id_type_supported? @@ -21,8 +23,8 @@ def id_type_supported? # @param [Object] classification_info assureid classification info # @param [String] doc_side value of ['Front', 'Back'] def doc_side_class_ok?(classification_info, doc_side) - side_type = classification_info&.with_indifferent_access&.dig(doc_side, 'ClassName') - !side_type&.present? || + side_type = classification_info&.with_indifferent_access&.dig(doc_side, :ClassName) + !side_type.present? || DocAuth::Response::ID_TYPE_SLUGS.key?(side_type) || side_type == 'Unknown' end @@ -30,11 +32,18 @@ def doc_side_class_ok?(classification_info, doc_side) # @param [Object] classification_info assureid classification info # @param [String] doc_side value of ['Front', 'Back'] def doc_issuing_country_ok?(classification_info, doc_side) - side_country = classification_info&.with_indifferent_access&.dig(doc_side, 'CountryCode') - !side_country&.present? || + side_country = classification_info&.with_indifferent_access&.dig(doc_side, :CountryCode) + !side_country.present? || supported_country_codes.include?(side_country) end + def doc_issuer_type_ok?(classification_info, doc_side) + side_issuer_type = classification_info&.with_indifferent_access&.dig(doc_side, :IssuerType) + side_issuer_type == DocAuth::Acuant::IssuerTypes::STATE_OR_PROVINCE.name || + side_issuer_type == DocAuth::Acuant::IssuerTypes::UNKNOWN.name || + !side_issuer_type.present? + end + def supported_country_codes IdentityConfig.store.doc_auth_supported_country_codes end diff --git a/app/services/doc_auth/error_generator.rb b/app/services/doc_auth/error_generator.rb index 4b8f7df0ace..7f1f521ce16 100644 --- a/app/services/doc_auth/error_generator.rb +++ b/app/services/doc_auth/error_generator.rb @@ -56,12 +56,12 @@ def generate_doc_auth_errors(response_info) unknown_fail_count = scan_for_unknown_alerts(response_info) alert_error_count -= unknown_fail_count - image_metric_errors = get_image_metric_errors(response_info[:image_metrics]) - return image_metric_errors.to_h unless image_metric_errors.empty? - doc_type_errors = get_id_type_errors(response_info[:classification_info]) return doc_type_errors.to_h unless doc_type_errors.nil? || doc_type_errors.empty? + image_metric_errors = get_image_metric_errors(response_info[:image_metrics]) + return image_metric_errors.to_h unless image_metric_errors.empty? + alert_errors = get_error_messages(response_info) error = '' @@ -109,12 +109,17 @@ def get_id_type_errors(classification_info) %w[Front Back].each do |side| side_class = classification_info.with_indifferent_access.dig(side, 'ClassName') side_country = classification_info.with_indifferent_access.dig(side, 'CountryCode') + side_issuer_type = classification_info.with_indifferent_access.dig(side, 'IssuerType') + side_ok = !side_class.present? || SUPPORTED_ID_CLASSNAME.include?(side_class) || side_class == 'Unknown' country_ok = !side_country.present? || supported_country_codes.include?(side_country) - both_side_ok &&= side_ok && country_ok - error_result.add_side(side.downcase.to_sym) unless side_ok && country_ok + issuer_type_ok = !side_issuer_type.present? || + side_issuer_type == DocAuth::Acuant::IssuerTypes::STATE_OR_PROVINCE.name || + side_issuer_type == DocAuth::Acuant::IssuerTypes::UNKNOWN.name + both_side_ok &&= issuer_type_ok && side_ok && country_ok + error_result.add_side(side.downcase.to_sym) unless side_ok && issuer_type_ok && country_ok end unless both_side_ok error_result.set_error(Errors::DOC_TYPE_CHECK) diff --git a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb index 731bf9961b3..e9291a4cc77 100644 --- a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb +++ b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb @@ -65,7 +65,7 @@ def initialize(http_response, config) end def successful_result? - all_passed? || attention_with_barcode? + (all_passed? || attention_with_barcode?) && id_type_supported? end def error_messages @@ -247,17 +247,23 @@ def doc_class_name true_id_product&.dig(:AUTHENTICATION_RESULT, :DocClassName) end + def doc_issuer_type + true_id_product&.dig(:AUTHENTICATION_RESULT, :DocIssuerType) + end + def classification_info - # Acuent response has both sides info, here simulate that + # Acuant response has both sides info, here simulate that doc_class = doc_class_name issuing_country = pii_from_doc[:issuing_country_code] { Front: { ClassName: doc_class, + IssuerType: doc_issuer_type, CountryCode: issuing_country, }, Back: { ClassName: doc_class, + IssuerType: doc_issuer_type, CountryCode: issuing_country, }, } diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb index e3cb88ad448..a39f11ddb83 100644 --- a/app/services/doc_auth/mock/result_response.rb +++ b/app/services/doc_auth/mock/result_response.rb @@ -30,17 +30,17 @@ def errors {} else doc_auth_result = file_data.dig('doc_auth_result') - # Error generator is not to be called when it's not failure - # allows us to test successful results - return {} if doc_auth_result == 'Passed' image_metrics = file_data.dig('image_metrics') failed = file_data.dig('failed_alerts') passed = file_data.dig('passed_alerts') liveness_result = file_data.dig('liveness_result') classification_info = file_data.dig('classification_info') - + # Pass and doc type is ok if [doc_auth_result, image_metrics, failed, passed, liveness_result, classification_info].any?(&:present?) + # Error generator is not to be called when it's not failure + # allows us to test successful results + return {} if doc_auth_result == 'Passed' && id_type_supported? mock_args = {} mock_args[:doc_auth_result] = doc_auth_result if doc_auth_result.present? mock_args[:image_metrics] = image_metrics.symbolize_keys if image_metrics.present? @@ -48,7 +48,6 @@ def errors mock_args[:passed] = passed.map!(&:symbolize_keys) if passed.present? mock_args[:liveness_result] = liveness_result if liveness_result.present? mock_args[:classification_info] = classification_info if classification_info.present? - fake_response_info = create_response_info(**mock_args) ErrorGenerator.new(config).generate_doc_auth_errors(fake_response_info) elsif file_data.include?(:general) # general is the key for errors from parsing @@ -68,7 +67,7 @@ def pii_from_doc end def success? - errors.blank? || attention_with_barcode? + (errors.blank? || attention_with_barcode?) && id_type_supported? end def attention_with_barcode? diff --git a/app/services/form_response.rb b/app/services/form_response.rb index 1804a571d26..e7947483ffd 100644 --- a/app/services/form_response.rb +++ b/app/services/form_response.rb @@ -56,7 +56,9 @@ def merge_arrays(_key, first, second) end def flatten_details(details) - details.transform_values { |errors| errors.pluck(:error) } + details.transform_values do |errors| + errors.map { |error| error[:type] || error[:error] }.index_with(true) + end end attr_reader :success diff --git a/app/services/idv/flows/in_person_flow.rb b/app/services/idv/flows/in_person_flow.rb index 76827bf6ddc..31f1626cc1d 100644 --- a/app/services/idv/flows/in_person_flow.rb +++ b/app/services/idv/flows/in_person_flow.rb @@ -47,7 +47,10 @@ def self.session_idv(session) def extra_analytics_properties extra = { - pii_like_keypaths: [[:same_address_as_id], [:state_id, :state_id_jurisdiction]], + pii_like_keypaths: [ + [:same_address_as_id], + [:proofing_results, :context, :stages, :state_id, :state_id_jurisdiction], + ], } unless @flow_session[:pii_from_user]&.[](:same_address_as_id).nil? extra[:same_address_as_id] = diff --git a/app/services/idv/steps/threat_metrix_step_helper.rb b/app/services/idv/steps/threat_metrix_step_helper.rb index 1723158478b..17b894bd81e 100644 --- a/app/services/idv/steps/threat_metrix_step_helper.rb +++ b/app/services/idv/steps/threat_metrix_step_helper.rb @@ -57,17 +57,10 @@ def log_irs_tmx_fraud_check_event(result, user) user: user, login_session_id: Digest::SHA1.hexdigest(user.unique_session_id.to_s), ) - - if (tmx_summary_reason_code = result.dig(:response_body, :tmx_summary_reason_code)) - failure_reason = { - tmx_summary_reason_code: tmx_summary_reason_code, - } - end end irs_attempts_api_tracker.idv_tmx_fraud_check( success: success, - failure_reason: failure_reason, ) end end diff --git a/app/services/irs_attempts_api/tracker.rb b/app/services/irs_attempts_api/tracker.rb index 463ec760cdb..2d0fc63cf23 100644 --- a/app/services/irs_attempts_api/tracker.rb +++ b/app/services/irs_attempts_api/tracker.rb @@ -4,9 +4,5 @@ class Tracker def track_event(event_type, metadata = {}) end - - def parse_failure_reason(result) - return result.to_h[:error_details] || result.errors.presence - end end end diff --git a/app/services/irs_attempts_api/tracker_events.rb b/app/services/irs_attempts_api/tracker_events.rb index 07003dca817..fa43c36cb42 100644 --- a/app/services/irs_attempts_api/tracker_events.rb +++ b/app/services/irs_attempts_api/tracker_events.rb @@ -3,13 +3,11 @@ module IrsAttemptsApi module TrackerEvents # @param [Boolean] success True if Account Successfully Deleted - # @param [Hash>] failure_reason displays why account deletion failed # A User confirms and deletes their Login.gov account after 24 hour period - def account_reset_account_deleted(success:, failure_reason: nil) + def account_reset_account_deleted(success:) track_event( :account_reset_account_deleted, success: success, - failure_reason: failure_reason, ) end @@ -30,12 +28,10 @@ def account_reset_request_submitted(success:) end # @param [Boolean] success - # @param [Hash>] failure_reason - def forgot_password_email_confirmed(success:, failure_reason: nil) + def forgot_password_email_confirmed(success:) track_event( :forgot_password_email_confirmed, success: success, - failure_reason: failure_reason, ) end @@ -58,12 +54,10 @@ def forgot_password_email_sent(email:) end # @param [Boolean] success - # @param [Hash>] failure_reason - def forgot_password_new_password_submitted(success:, failure_reason: nil) + def forgot_password_new_password_submitted(success:) track_event( :forgot_password_new_password_submitted, success: success, - failure_reason: failure_reason, ) end @@ -108,7 +102,6 @@ def idv_document_upload_rate_limited # @param [String] last_name # @param [String] date_of_birth # @param [String] address - # @param [Hash>] failure_reason # The document was uploaded during the IDV process def idv_document_upload_submitted( success:, @@ -122,8 +115,7 @@ def idv_document_upload_submitted( first_name: nil, last_name: nil, date_of_birth: nil, - address: nil, - failure_reason: nil + address: nil ) track_event( :idv_document_upload_submitted, @@ -139,7 +131,6 @@ def idv_document_upload_submitted( last_name: last_name, date_of_birth: date_of_birth, address: address, - failure_reason: failure_reason, ) end @@ -160,13 +151,11 @@ def idv_gpo_verification_rate_limited end # @param [Boolean] success - # @param [Hash>] failure_reason displays GPO submission failed # GPO verification submitted from Letter sent to verify address - def idv_gpo_verification_submitted(success:, failure_reason: nil) + def idv_gpo_verification_submitted(success:) track_event( :idv_gpo_verification_submitted, success: success, - failure_reason: failure_reason, ) end @@ -189,15 +178,13 @@ def idv_personal_key_generated # @param [Boolean] success # @param [String] phone_number # @param [String] otp_delivery_method - Either SMS or Voice - # @param [Hash>] failure_reason # Track when OTP is sent and what method chosen during idv flow. - def idv_phone_otp_sent(success:, phone_number:, otp_delivery_method:, failure_reason: nil) + def idv_phone_otp_sent(success:, phone_number:, otp_delivery_method:) track_event( :idv_phone_otp_sent, success: success, phone_number: phone_number, otp_delivery_method: otp_delivery_method, - failure_reason: failure_reason, ) end @@ -211,13 +198,11 @@ def idv_phone_otp_sent_rate_limited # Tracks when a user submits OTP code sent to their phone # @param [Boolean] success # @param [String] phone_number - # @param [Hash>] failure_reason - def idv_phone_otp_submitted(success:, phone_number:, failure_reason: nil) + def idv_phone_otp_submitted(success:, phone_number:) track_event( :idv_phone_otp_submitted, success: success, phone_number: phone_number, - failure_reason: failure_reason, ) end @@ -242,30 +227,25 @@ def idv_phone_send_link_rate_limited(phone_number:) # Tracks when the user submits their idv phone number # @param [Boolean] success # @param [String] phone_number - # @param [Hash>] failure_reason - def idv_phone_submitted(success:, phone_number:, failure_reason: nil) + def idv_phone_submitted(success:, phone_number:) track_event( :idv_phone_submitted, success: success, phone_number: phone_number, - failure_reason: failure_reason, ) end # @param [Boolean] success # @param [String] phone_number - # @param [Hash>] failure_reason # The phone number that the link was sent to during the IDV process def idv_phone_upload_link_sent( success:, - phone_number:, - failure_reason: nil + phone_number: ) track_event( :idv_phone_upload_link_sent, success: success, phone_number: phone_number, - failure_reason: failure_reason, ) end @@ -286,14 +266,12 @@ def idv_ssn_submitted(ssn:) end # @param [Boolean] success - # @param [Hash>] failure_reason # This event will capture the result of the TMX fraud check # during Identity Verification - def idv_tmx_fraud_check(success:, failure_reason: nil) + def idv_tmx_fraud_check(success:) track_event( :idv_tmx_fraud_check, success: success, - failure_reason: failure_reason, ) end @@ -316,7 +294,6 @@ def idv_verification_rate_limited(limiter_context:) # @param [String] date_of_birth # @param [String] address # @param [String] ssn - # @param [Hash>] failure_reason # The verification was submitted during the IDV process def idv_verification_submitted( success:, @@ -328,8 +305,7 @@ def idv_verification_submitted( last_name: nil, date_of_birth: nil, address: nil, - ssn: nil, - failure_reason: nil + ssn: nil ) track_event( :idv_verification_submitted, @@ -343,7 +319,6 @@ def idv_verification_submitted( date_of_birth: date_of_birth, address: address, ssn: ssn, - failure_reason: failure_reason, ) end @@ -357,13 +332,11 @@ def logged_in_account_purged(success:) end # @param [Boolean] success True if the password was successfully changed - # @param [Hash>] failure_reason # A logged-in user has attempted to change their password - def logged_in_password_change(success:, failure_reason: nil) + def logged_in_password_change(success:) track_event( :logged_in_password_change, success: success, - failure_reason: failure_reason, ) end @@ -462,17 +435,14 @@ def mfa_enroll_phone_otp_submitted(success:) # Tracks when the user has attempted to enroll the piv cac MFA method to their account # @param [Boolean] success # @param [String] subject_dn - # @param [Hash>] failure_reason def mfa_enroll_piv_cac( success:, - subject_dn: nil, - failure_reason: nil + subject_dn: nil ) track_event( :mfa_enroll_piv_cac, success: success, subject_dn: subject_dn, - failure_reason: failure_reason, ) end @@ -526,14 +496,12 @@ def mfa_login_backup_code(success:) # @param [Boolean] reauthentication - True if the user was already logged in # @param [String] phone_number - The user's phone_number used for multi-factor authentication # @param [String] otp_delivery_method - Either SMS or Voice - # @param [Hash>] failure_reason - reason for failure if success is false # During a login attempt, an OTP code has been sent via SMS or Voice. def mfa_login_phone_otp_sent( success:, reauthentication:, phone_number:, - otp_delivery_method:, - failure_reason: + otp_delivery_method: ) track_event( :mfa_login_phone_otp_sent, @@ -541,7 +509,6 @@ def mfa_login_phone_otp_sent( reauthentication: reauthentication, phone_number: phone_number, otp_delivery_method: otp_delivery_method, - failure_reason: failure_reason, ) end @@ -569,17 +536,14 @@ def mfa_login_phone_otp_submitted(reauthentication:, success:) # Tracks when the user has attempted to log in with the piv cac MFA method to their account # @param [Boolean] success # @param [String] subject_dn - # @param [Hash>] failure_reason def mfa_login_piv_cac( success:, - subject_dn: nil, - failure_reason: nil + subject_dn: nil ) track_event( :mfa_login_piv_cac, success: success, subject_dn: subject_dn, - failure_reason: failure_reason, ) end @@ -629,29 +593,24 @@ def personal_key_reactivation_rate_limited # Tracks when user has entered personal key after forgot password steps # @param [Boolean] success - # @param [Hash>] failure_reason - def personal_key_reactivation_submitted(success:, failure_reason: nil) + def personal_key_reactivation_submitted(success:) track_event( :personal_key_reactivation_submitted, success: success, - failure_reason: failure_reason, ) end # Tracks when user confirms registration email # @param [Boolean] success # @param [String] email - # @param [Hash>] failure_reason def user_registration_email_confirmation( success:, - email: nil, - failure_reason: nil + email: nil ) track_event( :user_registration_email_confirmation, success: success, email: email, - failure_reason: failure_reason, ) end @@ -672,31 +631,20 @@ def user_registration_email_submission_rate_limited( # Tracks when user submits registration email # @param [Boolean] success # @param [String] email - # @param [Hash>] failure_reason - def user_registration_email_submitted( - success:, - email:, - failure_reason: nil - ) + def user_registration_email_submitted(success:, email:) track_event( :user_registration_email_submitted, success: success, email: email, - failure_reason: failure_reason, ) end # Tracks when user submits registration password # @param [Boolean] success - # @param [Hash>] failure_reason - def user_registration_password_submitted( - success:, - failure_reason: nil - ) + def user_registration_password_submitted(success:) track_event( :user_registration_password_submitted, success: success, - failure_reason: failure_reason, ) end end diff --git a/app/services/password_reset_token_validator.rb b/app/services/password_reset_token_validator.rb index 3ffce440c73..6b410353de8 100644 --- a/app/services/password_reset_token_validator.rb +++ b/app/services/password_reset_token_validator.rb @@ -17,6 +17,7 @@ def submit attr_accessor :user def valid_token - errors.add(:user, 'token_expired', type: :user) unless user.reset_password_period_valid? + return if user.reset_password_period_valid? + errors.add(:user, 'token_expired', type: :token_expired) end end diff --git a/app/services/push_notification/account_disabled_event.rb b/app/services/push_notification/account_disabled_event.rb new file mode 100644 index 00000000000..d35ec8acd8a --- /dev/null +++ b/app/services/push_notification/account_disabled_event.rb @@ -0,0 +1,31 @@ +module PushNotification + # This is used for account suspension + class AccountDisabledEvent + EVENT_TYPE = 'https://schemas.openid.net/secevent/risc/event-type/account-disabled'.freeze + + attr_reader :user + + def initialize(user:) + @user = user + end + + def event_type + EVENT_TYPE + end + + def payload(iss_sub:) + { + subject: { + subject_type: 'iss-sub', + iss: Rails.application.routes.url_helpers.root_url, + sub: iss_sub, + }, + reason: 'account-suspension', + } + end + + def ==(other) + self.class == other.class && user == other.user + end + end +end diff --git a/app/services/push_notification/account_enabled_event.rb b/app/services/push_notification/account_enabled_event.rb new file mode 100644 index 00000000000..759d2436230 --- /dev/null +++ b/app/services/push_notification/account_enabled_event.rb @@ -0,0 +1,18 @@ +module PushNotification + # This is used for account reinstatement + class AccountEnabledEvent + include IssSubEvent + + EVENT_TYPE = 'https://schemas.openid.net/secevent/risc/event-type/account-enabled'.freeze + + attr_reader :user + + def initialize(user:) + @user = user + end + + def event_type + EVENT_TYPE + end + end +end diff --git a/app/services/user_profiles_encryptor.rb b/app/services/user_profiles_encryptor.rb new file mode 100644 index 00000000000..5f86d3bf58a --- /dev/null +++ b/app/services/user_profiles_encryptor.rb @@ -0,0 +1,28 @@ +class UserProfilesEncryptor + attr_reader :personal_key + + def initialize(user:, user_session:, password:) + @user = user + @user_session = user_session + @password = password + end + + def encrypt + if user.active_profile.present? + encrypt_pii_for_profile(user.active_profile) + end + if user.pending_profile.present? + encrypt_pii_for_profile(user.pending_profile) + end + end + + private + + attr_reader :user, :password, :user_session + + def encrypt_pii_for_profile(profile) + pii = Pii::Cacher.new(user, user_session).fetch(profile.id) + @personal_key = profile.encrypt_pii(pii, password) + profile.save! + end +end diff --git a/app/views/idv/address/new.html.erb b/app/views/idv/address/new.html.erb index 6350d594468..80358753a39 100644 --- a/app/views/idv/address/new.html.erb +++ b/app/views/idv/address/new.html.erb @@ -87,4 +87,4 @@ <%= render 'idv/shared/back', step: 'verify' %> <%= javascript_packs_tag_once('formatted-fields') %> -<%= javascript_packs_tag_once('puerto-rico-guidance') %> +<%= javascript_packs_tag_once('state-guidance') %> diff --git a/app/views/idv/in_person/address.html.erb b/app/views/idv/in_person/address.html.erb index ce1dea95a47..493ae0ef18e 100644 --- a/app/views/idv/in_person/address.html.erb +++ b/app/views/idv/in_person/address.html.erb @@ -83,4 +83,4 @@ <%= render 'idv/doc_auth/cancel', step: 'address' %> <% end %> -<%= javascript_packs_tag_once('formatted-fields', 'puerto-rico-guidance') %> +<%= javascript_packs_tag_once('formatted-fields', 'state-guidance') %> diff --git a/app/views/idv/in_person/address/show.html.erb b/app/views/idv/in_person/address/show.html.erb index 0e3cc41d9c6..580240a8613 100644 --- a/app/views/idv/in_person/address/show.html.erb +++ b/app/views/idv/in_person/address/show.html.erb @@ -92,4 +92,4 @@ <%= render 'idv/doc_auth/cancel', step: 'address' %> <% end %> -<%= javascript_packs_tag_once('formatted-fields', 'puerto-rico-guidance') %> +<%= javascript_packs_tag_once('formatted-fields', 'state-guidance') %> diff --git a/app/views/idv/in_person/state_id.html.erb b/app/views/idv/in_person/state_id.html.erb index 9c129515d07..a22d2a03829 100644 --- a/app/views/idv/in_person/state_id.html.erb +++ b/app/views/idv/in_person/state_id.html.erb @@ -102,6 +102,7 @@ collection: us_states_territories, form: f, hint: t('in_person_proofing.form.state_id.state_id_jurisdiction_hint'), + input_html: { class: 'jurisdiction-state-selector' }, label: t('in_person_proofing.form.state_id.state_id_jurisdiction'), label_html: { class: 'usa-label' }, prompt: t('in_person_proofing.form.state_id.state_id_jurisdiction_prompt'), @@ -110,11 +111,38 @@ ) %>
    + <% state_id_number_hint_default = capture do %> + <%= t('in_person_proofing.form.state_id.state_id_number_hint') %> + <% [ + [t('in_person_proofing.form.state_id.state_id_number_hint_spaces'), ' '], + [t('in_person_proofing.form.state_id.state_id_number_hint_forward_slashes'), '/'], + [t('in_person_proofing.form.state_id.state_id_number_hint_asterisks'), '*'], + [t('in_person_proofing.form.state_id.state_id_number_hint_dashes'), '-', true], + ].each do |text, symbol, last| %> + <%= text %><%= ',' if !last %> + + <% end %> + <% end %> + + <% state_id_number_hint = capture do %> + <% [ + [:default, state_id_number_hint_default], + ['TX', t('in_person_proofing.form.state_id.state_id_number_texas_hint')], + ].each do |state, hint| %> + <%= content_tag( + :span, + hint, + class: state == :default ? nil : 'display-none', + data: { state: }, + ) %> + <% end %> + <% end %> + <%= render ValidatedFieldComponent.new( name: :state_id_number, form: f, - hint: t('in_person_proofing.form.state_id.state_id_number_hint_html'), - hint_html: { class: 'tablet:grid-col-10' }, + hint: state_id_number_hint, + hint_html: { class: ['tablet:grid-col-10', 'jurisdiction-extras'] }, input_html: { value: pii[:state_id_number] }, label: t('in_person_proofing.form.state_id.state_id_number'), label_html: { class: 'usa-label' }, @@ -211,4 +239,4 @@ <% else %> <%= render 'idv/doc_auth/cancel', step: 'state_id' %> <% end %> -<%= javascript_packs_tag_once('formatted-fields', 'puerto-rico-guidance') %> +<%= javascript_packs_tag_once('formatted-fields', 'state-guidance') %> diff --git a/app/views/sign_up/completions/_requested_attributes.html.erb b/app/views/sign_up/completions/_requested_attributes.html.erb deleted file mode 100644 index 3933b2379a7..00000000000 --- a/app/views/sign_up/completions/_requested_attributes.html.erb +++ /dev/null @@ -1,32 +0,0 @@ -
      - <% pii.each do |attribute_key, attribute_value| %> - <% next if attribute_value.blank? %> -
    • -
      - <%= t("help_text.requested_attributes.#{attribute_key}") %> -
      -
      - <% if attribute_value.is_a? Array %> -
        - <% attribute_value.each do |item| %> -
      • <%= item %>
      • - <% end %> -
      - <% elsif attribute_key == :social_security_number %> - <%= render( - 'shared/masked_text', - text: attribute_value, - masked_text: SsnFormatter.format_masked(attribute_value), - accessible_masked_text: t( - 'idv.accessible_labels.masked_ssn', - first_number: attribute_value[0], - last_number: attribute_value[-1], - ), - ) %> - <% else %> - <%= attribute_value.to_s %> - <% end %> -
      -
    • - <% end %> -
    diff --git a/app/views/sign_up/completions/show.html.erb b/app/views/sign_up/completions/show.html.erb index c4b22e8af79..86d6e4bf864 100644 --- a/app/views/sign_up/completions/show.html.erb +++ b/app/views/sign_up/completions/show.html.erb @@ -3,16 +3,44 @@ <%= image_tag asset_url(@presenter.image_name), width: 140, alt: @presenter.image_alt, class: 'margin-bottom-2' %>
    -<%= render PageHeadingComponent.new(class: 'tablet:margin-right-1 tablet:margin-left-1 text-center') do %> +<%= render PageHeadingComponent.new(class: 'text-center') do %> <%= @presenter.heading %> <% end %> -

    +

    <%= @presenter.intro.html_safe %>

    -
    - <%= render 'sign_up/completions/requested_attributes', pii: @presenter.pii %> -
    + +<%= render IconListComponent.new(icon: :check_circle, color: :success, class: 'border-bottom border-primary-light') do |c| %> + <% @presenter.pii.each do |attribute_key, attribute_value| %> + <% next if attribute_value.blank? %> + <% c.with_item(class: 'padding-y-2 border-top border-primary-light') do %> + + <%= t("help_text.requested_attributes.#{attribute_key}") %> + + <% if attribute_value.is_a? Array %> +
      + <% attribute_value.each do |item| %> +
    • <%= item %>
    • + <% end %> +
    + <% elsif attribute_key == :social_security_number %> + <%= render( + 'shared/masked_text', + text: attribute_value, + masked_text: SsnFormatter.format_masked(attribute_value), + accessible_masked_text: t( + 'idv.accessible_labels.masked_ssn', + first_number: attribute_value[0], + last_number: attribute_value[-1], + ), + ) %> + <% else %> + <%= attribute_value.to_s %> + <% end %> + <% end %> + <% end %> +<% end %> <% if !@multiple_factors_enabled %> <%= render(AlertComponent.new(type: :warning, class: 'margin-bottom-4')) do %> diff --git a/app/views/users/authorization_confirmation/new.html.erb b/app/views/users/authorization_confirmation/new.html.erb index 04d03ab86f3..ea34d93ec14 100644 --- a/app/views/users/authorization_confirmation/new.html.erb +++ b/app/views/users/authorization_confirmation/new.html.erb @@ -6,30 +6,34 @@ <%= render PageHeadingComponent.new.with_content(decorated_sp_session.new_session_heading) %> <% end %> -
    -
    - <%= t('user_authorization_confirmation.currently_logged_in') %> -
    -
    -
      -
    • - - <%= t('help_text.requested_attributes.email') %> - - - <%= @email %> - -
    • -
    -
    +
    + <%= t('user_authorization_confirmation.currently_logged_in') %> +
    +<%= render IconListComponent.new(icon: :check_circle, color: :success, class: 'border-bottom border-primary-light') do |c| %> + <% c.with_item(class: 'padding-y-2 border-top border-primary-light') do %> + + <%= t('help_text.requested_attributes.email') %> + + <%= @email %> + <% end %> +<% end %> -
    - <%= button_to(user_authorization_confirmation_path, class: 'usa-button usa-button--big usa-button--wide', method: :post) { t('user_authorization_confirmation.continue') } %> -
    -
    +
    + <%= render ButtonComponent.new( + action: ->(**tag_options, &block) do + button_to(user_authorization_confirmation_path, method: :post, **tag_options, &block) + end, + big: true, + wide: true, + ).with_content(t('user_authorization_confirmation.continue')) %> +
    <%= t('user_authorization_confirmation.or') %>
    -
    - <%= button_to(reset_user_authorization_path, class: 'usa-button usa-button--big usa-button--wide', method: :delete) { t('user_authorization_confirmation.sign_in') } %> -
    + <%= render ButtonComponent.new( + action: ->(**tag_options, &block) do + button_to(reset_user_authorization_path, method: :delete, **tag_options, &block) + end, + big: true, + wide: true, + ).with_content(t('user_authorization_confirmation.sign_in')) %>
    diff --git a/bin/aamva-test-cert b/bin/aamva-test-cert index 0d5ff682dae..fe66d0079db 100755 --- a/bin/aamva-test-cert +++ b/bin/aamva-test-cert @@ -3,38 +3,51 @@ ENV['LOGIN_TASK_LOG_LEVEL'] ||= 'warn' require_relative '../config/environment.rb' require 'aamva_test' - -auth_url = nil -verification_url = nil - -parser = OptionParser.new do |opts| - opts.banner = <<~EOM - Usage: #{$PROGRAM_NAME} --auth-url=AUTH_URL --verification-url=VERIFICATION_URL - - Tests AAMVA certificate against cert environment - - Options: - EOM - - opts.on('--auth-url=AUTH_URL', 'sets the auth url') do |url| - auth_url = url - end - - opts.on('--verification-url=VERIFICATION_URL', 'sets the verification url') do |url| - verification_url = url - end - - opts.on('--help', 'prints this help message') do - puts opts - exit 0 +require 'optparse' +require 'pp' + +class AamvaTestCert + def run(out: STDOUT, argv: ARGV) + auth_url = nil + verification_url = nil + show_help = false + + parser = OptionParser.new do |opts| + opts.banner = <<~EOM + Usage: #{$PROGRAM_NAME} --auth-url=AUTH_URL --verification-url=VERIFICATION_URL + + Tests AAMVA certificate against cert environment + + Options: + EOM + + opts.on('--auth-url=AUTH_URL', 'sets the auth url') do |url| + auth_url = url + end + + opts.on('--verification-url=VERIFICATION_URL', 'sets the verification url') do |url| + verification_url = url + end + + opts.on('--help', 'prints this help message') do + show_help = true + end + end + + parser.parse!(argv) + + if show_help + out.puts parser + exit 0 + elsif !auth_url || !verification_url + out.puts parser + exit 1 + else + PP.pp(AamvaTest.new.test_cert(auth_url:, verification_url:), out) + end end end -parser.parse!(ARGV) - -if !auth_url || !verification_url - puts parser - exit 1 +if $PROGRAM_NAME == __FILE__ + AamvaTestCert.new.run end - -puts AamvaTest.new.test_cert(auth_url:, verification_url:) diff --git a/bin/aamva-test-connectivity b/bin/aamva-test-connectivity index 9bf5c4cd461..bdb20e77ec1 100755 --- a/bin/aamva-test-connectivity +++ b/bin/aamva-test-connectivity @@ -4,4 +4,4 @@ ENV['LOGIN_TASK_LOG_LEVEL'] ||= 'warn' require_relative '../config/environment.rb' require 'aamva_test' -puts AamvaTest.new.test_connectivity +p AamvaTest.new.test_connectivity diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index 3c33cbf082f..ec51f0fdac9 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -188,6 +188,11 @@ cron: cron_24h, args: -> { [14.days.ago] }, }, + # Expire old GPO profiles + expire_gpo_profiles: { + class: 'GpoExpirationJob', + cron: cron_24h, + }, # Monthly report checking in on key metrics monthly_key_metrics_report: { class: 'Reports::MonthlyKeyMetricsReport', diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 99f7a8db1ad..5016c76171e 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -257,6 +257,7 @@ def headers ) do |_name, _start, _finish, _request_id, payload| req = payload[:request] user = req.env['warden'].user || AnonymousUser.new - analytics = Analytics.new(user: user, request: req, session: {}, sp: nil) + sp = req.env.fetch('rack.session', {}).dig('sp', 'issuer') + analytics = Analytics.new(user: user, request: req, session: {}, sp: sp) analytics.rate_limit_triggered(type: req.env['rack.attack.matched']) end diff --git a/config/locales/in_person_proofing/en.yml b/config/locales/in_person_proofing/en.yml index 2f91302f5e5..a0044835ed7 100644 --- a/config/locales/in_person_proofing/en.yml +++ b/config/locales/in_person_proofing/en.yml @@ -131,13 +131,13 @@ en: state_id_jurisdiction_hint: This is the state that issued your ID state_id_jurisdiction_prompt: '- Select -' state_id_number: ID number - state_id_number_hint_html: "May include letters, numbers, and the following - symbols: spaces, forward - slashes, asterisks, - dashes" + state_id_number_hint: 'May include letters, numbers, and the following symbols:' + state_id_number_hint_asterisks: asterisks + state_id_number_hint_dashes: dashes + state_id_number_hint_forward_slashes: forward slashes + state_id_number_hint_spaces: spaces + state_id_number_texas_hint: This is the 8-digit number on your ID. Enter only + numbers in this field. zipcode: ZIP Code headings: address: Enter your current residential address diff --git a/config/locales/in_person_proofing/es.yml b/config/locales/in_person_proofing/es.yml index 855cb080701..fbd23201e29 100644 --- a/config/locales/in_person_proofing/es.yml +++ b/config/locales/in_person_proofing/es.yml @@ -143,13 +143,13 @@ es: state_id_jurisdiction_hint: Este es el estado que emitió su identificación state_id_jurisdiction_prompt: '- Seleccione -' state_id_number: Número de identidad - state_id_number_hint_html: "Puede incluir letras, números y los siguientes - símbolos: espacios, barras - diagonales, asteriscos, guiones" + state_id_number_hint: 'Puede incluir letras, números y los siguientes símbolos:' + state_id_number_hint_asterisks: 'asteriscos' + state_id_number_hint_dashes: guiones + state_id_number_hint_forward_slashes: 'barras diagonal' + state_id_number_hint_spaces: 'espacios' + state_id_number_texas_hint: Este es el número de 8 dígitos de su ID. Introduzca + sólo números en este campo. zipcode: Código postal headings: address: Ingresa tu domicilio actual diff --git a/config/locales/in_person_proofing/fr.yml b/config/locales/in_person_proofing/fr.yml index 91657bdacb3..d73e40a2ef3 100644 --- a/config/locales/in_person_proofing/fr.yml +++ b/config/locales/in_person_proofing/fr.yml @@ -143,13 +143,13 @@ fr: state_id_jurisdiction_hint: Il s’agit de l’État qui a émis votre pièce d’identité state_id_jurisdiction_prompt: '- Sélectionnez -' state_id_number: Numéro d’identification - state_id_number_hint_html: "Il peut s’agir de lettres, de chiffres et des - symboles suivants: des espaces, des barres - obliques, des astérisques, des - tirets" + state_id_number_hint: 'Il peut s’agir de lettres, de chiffres et des symboles suivants:' + state_id_number_hint_asterisks: 'des astérisques' + state_id_number_hint_dashes: 'des tirets' + state_id_number_hint_forward_slashes: 'des barres obliques' + state_id_number_hint_spaces: 'des espaces' + state_id_number_texas_hint: Il s’agit du numéro à huit chiffres figurant sur + votre carte d’identité. Entrez uniquement des chiffres dans ce champ. zipcode: Code postal headings: address: Indiquez votre adresse résidentielle actuelle diff --git a/config/locales/two_factor_authentication/en.yml b/config/locales/two_factor_authentication/en.yml index 13a73ae2513..bac52964f9d 100644 --- a/config/locales/two_factor_authentication/en.yml +++ b/config/locales/two_factor_authentication/en.yml @@ -163,8 +163,6 @@ en: (toll) phone numbers. piv_cac: Government employee ID piv_cac_info: PIV/CAC cards for government and military employees. Desktop only. - sms: Text message / SMS - voice: Phone call webauthn: Security key webauthn_info: A physical device, often shaped like a USB drive, that you plug in to your device. diff --git a/config/locales/two_factor_authentication/es.yml b/config/locales/two_factor_authentication/es.yml index 2f079bd6027..c2a30013de9 100644 --- a/config/locales/two_factor_authentication/es.yml +++ b/config/locales/two_factor_authentication/es.yml @@ -173,8 +173,6 @@ es: piv_cac: Identificación de empleado gubernamental piv_cac_info: Credenciales PIV/CAC para empleados gubernamentales y del ejército. Únicamente versión de escritorio. - sms: Mensaje de texto / SMS - voice: Llamada telefónica webauthn: Clave de seguridad webauthn_info: Un dispositivo físico que suele tener la forma de una unidad USB y se conecta a su dispositivo. diff --git a/config/locales/two_factor_authentication/fr.yml b/config/locales/two_factor_authentication/fr.yml index ce17a3ce3a3..52dee824d9f 100644 --- a/config/locales/two_factor_authentication/fr.yml +++ b/config/locales/two_factor_authentication/fr.yml @@ -182,8 +182,6 @@ fr: piv_cac: Carte d’identification des employés du gouvernement piv_cac_info: Cartes PIV/CAC pour les fonctionnaires et les militaires. Bureau uniquement. - sms: SMS - voice: Appel téléphonique webauthn: Clef de sécurité webauthn_info: Un appareil physique, souvent en forme de clé USB, que vous branchez sur votre appareil. diff --git a/docs/frontend.md b/docs/frontend.md index fe85d57672b..af4f5c431a4 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -137,7 +137,7 @@ See [`@18f/identity-i18n` package documentation](../app/javascript/packages/i18n See [`@18f/identity-analytics` package documentation][analytics_package] for code examples detailing how to track an event in JavaScript. -Any event logged from the frontend must be added to the `EVENT_MAP` allowlist in [`FrontendLogController`][frontend_log_controller.rb]. +Any event logged from the frontend must be added to the `ALLOWED_EVENTS` allowlist in [`FrontendLogController`][frontend_log_controller.rb]. This mapping associates the event name logged from the frontend with the corresponding method from [AnalyticsEvents][analytics_events.rb] to be called. All properties will be passed automatically to the event from the frontend as long as they are defined in the method argument signature. @@ -147,16 +147,13 @@ in the frontend, such as an A/B test bucket descriptor. In these scenarios, you 1. Add the value to the page markup, such as through an [HTML `data-` attribute][data_attributes], and reference that attribute in JavaScript. -2. Define the mapped value in `EVENT_MAP` to a service class, such as how [frontend error logging][frontend_error_logging] -is implemented. -3. Implement a mixin to intercept and override the default behavior of an analytics event, such as +2. Implement a mixin to intercept and override the default behavior of an analytics event, such as how [`Idv::AnalyticsEventEnhancer`][analytics_events_enhancer.rb] is implemented. [analytics_package]: ../app/javascript/packages/analytics/README.md [frontend_log_controller.rb]: https://github.com/18F/identity-idp/blob/main/app/controllers/frontend_log_controller.rb [analytics_events.rb]: https://github.com/18F/identity-idp/blob/main/app/services/analytics_events.rb [data_attributes]: https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes -[frontend_error_logging]: https://github.com/18F/identity-idp/blob/9c17164c0b8d9b4aefad74dde1a521c111b53aac/app/controllers/frontend_log_controller.rb#L14 [analytics_events_enhancer.rb]: https://github.com/18F/identity-idp/blob/main/app/services/idv/analytics_events_enhancer.rb ## Components diff --git a/lib/tasks/backfill_gpo_expiration.rake b/lib/tasks/backfill_gpo_expiration.rake deleted file mode 100644 index 59d1db6f9d0..00000000000 --- a/lib/tasks/backfill_gpo_expiration.rake +++ /dev/null @@ -1,69 +0,0 @@ -namespace :profiles do - desc 'Backfill the gpo_verification_expired_at value' - - ## - # Usage: - # - # Print pending updates - # bundle exec rake profiles:backfill_gpo_expiration > profiles.csv - # - # Commit updates - # bundle exec rake profiles:backfill_gpo_expiration UPDATE_PROFILES=true > profiles.csv - # - task backfill_gpo_expiration: :environment do |_task, _args| - min_profile_age = (ENV['MIN_PROFILE_AGE_IN_DAYS'].to_i || 100).days - update_profiles = ENV['UPDATE_PROFILES'] == 'true' - statement_timeout = ENV['STATEMENT_TIMEOUT_IN_SECONDS'].to_i.seconds || 10.minutes - - count = 0 - earliest = nil - latest = nil - - on_profile_expired = ->(profile:, gpo_verification_pending_at:) do - count += 1 - - earliest = [earliest, gpo_verification_pending_at].compact.min - latest = [latest, gpo_verification_pending_at].compact.max - - puts "#{profile.id},#{gpo_verification_pending_at.iso8601}" - - if count % 100 == 0 - verb = update_profiles ? 'Expired' : 'Found' - warn "#{verb} #{count} profiles (earliest: #{earliest}, latest: #{latest})" - end - end - - job = GpoExpirationJob.new(on_profile_expired: on_profile_expired) - - job.perform( - now: Time.zone.now, - min_profile_age: min_profile_age, - dry_run: !update_profiles, - statement_timeout: statement_timeout, - ) - end - - ## - # Usage: - # - # Rollback the above: - # - # bundle exec rake profiles:rollback_backfill_gpo_expiration < profiles.csv - # - task rollback_backfill_gpo_expiration: :environment do |_task, _args| - profile_data = STDIN.read.split("\n").map do |profile_row| - profile_row.split(',') - end - - warn "Updating #{profile_data.count} records" - - profile_data.each do |profile_datum| - profile_id, gpo_verification_pending_at = profile_datum - Profile.where(id: profile_id).update!( - gpo_verification_pending_at: Time.zone.parse(gpo_verification_pending_at), - gpo_verification_expired_at: nil, - ) - warn profile_id - end - end -end diff --git a/package.json b/package.json index 81f6639a0bb..e0d12283ae6 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "mocha": "^10.0.0", "mq-polyfill": "^1.1.8", "msw": "^1.3.2", - "prettier": "^3.0.3", + "prettier": "^3.1.0", "quibble": "^0.6.17", "react-test-renderer": "^17.0.2", "sinon": "^14.0.0", diff --git a/spec/bin/aamva-test-cert_spec.rb b/spec/bin/aamva-test-cert_spec.rb new file mode 100644 index 00000000000..12658ac3abc --- /dev/null +++ b/spec/bin/aamva-test-cert_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' +load Rails.root.join('bin/aamva-test-cert') + +RSpec.describe AamvaTestCert do + let(:fake_aamva_test) do + Class.new do + def test_cert(auth_url:, verification_url:) + [auth_url, verification_url] + end + end + end + + before { stub_const('AamvaTest', fake_aamva_test) } + + subject(:instance) { AamvaTestCert.new } + + describe '#run' do + subject(:run) { instance.run(out: out, argv: argv) } + let(:out) { StringIO.new('') } + let(:argv) { [] } + + context 'missing arguments' do + let(:argv) { [] } + + it 'exits uncleanly' do + expect(instance).to receive(:exit).with(1) + + run + end + end + + context '--help' do + let(:argv) { %w[--help] } + + it 'exits cleanly and prints help' do + expect(instance).to receive(:exit).with(0) + + run + + expect(out.string).to include('Usage:') + end + end + + context 'required arguments' do + let(:argv) { %w[--auth-url a --verification-url b] } + + it 'pretty-prints the result' do + run + + expect(JSON.parse(out.string)).to eq(%w[a b]) + end + end + end +end diff --git a/spec/components/icon_list_component_spec.rb b/spec/components/icon_list_component_spec.rb index b414b631c23..599ede1b38c 100644 --- a/spec/components/icon_list_component_spec.rb +++ b/spec/components/icon_list_component_spec.rb @@ -16,9 +16,11 @@ end context 'with additional tag options' do - it 'applies tag options to wrapper element' do - rendered = render_inline IconListComponent.new(class: 'custom-class', data: { foo: 'bar' }) + subject(:rendered) do + render_inline IconListComponent.new(class: 'custom-class', data: { foo: 'bar' }) + end + it 'applies tag options to wrapper element' do expect(rendered).to have_css('.usa-icon-list.custom-class[data-foo="bar"]') end end @@ -65,5 +67,17 @@ expect(rendered).to have_css('.usa-icon use[href$=".svg#cancel"]', count: 1) end end + + context 'with additional tag options on item' do + subject(:rendered) do + render_inline IconListComponent.new(icon: :cancel) do |c| + c.with_item(class: 'custom-class', data: { foo: 'bar' }) { 'First' } + end + end + + it 'applies tag options to wrapper element' do + expect(rendered).to have_css('.usa-icon-list__item.custom-class[data-foo="bar"]') + end + end end end diff --git a/spec/controllers/account_reset/cancel_controller_spec.rb b/spec/controllers/account_reset/cancel_controller_spec.rb index 488f187ad25..41677ed6cd4 100644 --- a/spec/controllers/account_reset/cancel_controller_spec.rb +++ b/spec/controllers/account_reset/cancel_controller_spec.rb @@ -39,7 +39,7 @@ success: false, errors: { token: [t('errors.account_reset.cancel_token_invalid', app_name: APP_NAME)] }, error_details: { - token: [t('errors.account_reset.cancel_token_invalid', app_name: APP_NAME)], + token: { cancel_token_invalid: true }, }, user_id: 'anonymous-uuid', } @@ -55,7 +55,7 @@ analytics_hash = { success: false, errors: { token: [t('errors.account_reset.cancel_token_missing', app_name: APP_NAME)] }, - error_details: { token: [:blank] }, + error_details: { token: { blank: true } }, user_id: 'anonymous-uuid', } @@ -100,7 +100,7 @@ success: false, errors: { token: [t('errors.account_reset.cancel_token_invalid', app_name: APP_NAME)] }, error_details: { - token: [t('errors.account_reset.cancel_token_invalid', app_name: APP_NAME)], + token: { cancel_token_invalid: true }, }, } expect(@analytics).to receive(:track_event). diff --git a/spec/controllers/account_reset/delete_account_controller_spec.rb b/spec/controllers/account_reset/delete_account_controller_spec.rb index 1f02030e28b..4f147b4ddf9 100644 --- a/spec/controllers/account_reset/delete_account_controller_spec.rb +++ b/spec/controllers/account_reset/delete_account_controller_spec.rb @@ -41,7 +41,7 @@ user_id: 'anonymous-uuid', success: false, errors: invalid_token_error, - error_details: invalid_token_error, + error_details: { token: { granted_token_invalid: true } }, mfa_method_counts: {}, pii_like_keypaths: [[:mfa_method_counts, :phone]], account_age_in_days: 0, @@ -60,7 +60,7 @@ user_id: 'anonymous-uuid', success: false, errors: { token: [t('errors.account_reset.granted_token_missing', app_name: APP_NAME)] }, - error_details: { token: [:blank] }, + error_details: { token: { blank: true } }, mfa_method_counts: {}, pii_like_keypaths: [[:mfa_method_counts, :phone]], account_age_in_days: 0, @@ -86,9 +86,7 @@ user_id: user.uuid, success: false, errors: { token: [t('errors.account_reset.granted_token_expired', app_name: APP_NAME)] }, - error_details: { - token: [t('errors.account_reset.granted_token_expired', app_name: APP_NAME)], - }, + error_details: { token: { granted_token_expired: true } }, mfa_method_counts: {}, pii_like_keypaths: [[:mfa_method_counts, :phone]], account_age_in_days: 2, @@ -114,7 +112,7 @@ user_id: 'anonymous-uuid', success: false, errors: invalid_token_error, - error_details: invalid_token_error, + error_details: { token: { granted_token_invalid: true } }, } expect(@analytics).to receive(:track_event). with('Account Reset: granted token validation', properties) @@ -134,9 +132,7 @@ user_id: user.uuid, success: false, errors: { token: [t('errors.account_reset.granted_token_expired', app_name: APP_NAME)] }, - error_details: { - token: [t('errors.account_reset.granted_token_expired', app_name: APP_NAME)], - }, + error_details: { token: { granted_token_expired: true } }, } expect(@analytics).to receive(:track_event). with('Account Reset: granted token validation', properties) diff --git a/spec/controllers/frontend_log_controller_spec.rb b/spec/controllers/frontend_log_controller_spec.rb index 0025499ddd7..ae08ab4e70e 100644 --- a/spec/controllers/frontend_log_controller_spec.rb +++ b/spec/controllers/frontend_log_controller_spec.rb @@ -1,6 +1,19 @@ require 'rails_helper' RSpec.describe FrontendLogController do + describe '.LEGACY_EVENT_MAP' do + it 'has keys sorted alphabetically' do + expect(described_class::LEGACY_EVENT_MAP.keys). + to eq(described_class::LEGACY_EVENT_MAP.keys.sort_by(&:downcase)) + end + end + + describe '.ALLOWED_EVENTS' do + it 'is sorted alphabetically' do + expect(described_class::ALLOWED_EVENTS).to eq(described_class::ALLOWED_EVENTS.sort) + end + end + describe '#create' do subject(:action) { post :create, params: params, as: :json } @@ -219,12 +232,5 @@ end end end - - context 'with all events' do - it 'sorts keys alphabetically' do - expect(described_class::EVENT_MAP.keys). - to eq(described_class::EVENT_MAP.keys.sort_by(&:downcase)) - end - end end end diff --git a/spec/controllers/idv/by_mail/enter_code_controller_spec.rb b/spec/controllers/idv/by_mail/enter_code_controller_spec.rb index f5bd87d0052..9bc0a47e173 100644 --- a/spec/controllers/idv/by_mail/enter_code_controller_spec.rb +++ b/spec/controllers/idv/by_mail/enter_code_controller_spec.rb @@ -166,8 +166,7 @@ describe '#create' do let(:otp_code_error_message) { { otp: [t('errors.messages.confirmation_code_incorrect')] } } - let(:otp_code_incorrect) { { otp: [:confirmation_code_incorrect] } } - let(:success_properties) { { success: true, failure_reason: nil } } + let(:success_properties) { { success: true } } subject(:action) do post( @@ -387,11 +386,11 @@ which_letter: nil, letter_count: 1, attempts: 1, - error_details: otp_code_incorrect, + error_details: { otp: { confirmation_code_incorrect: true } }, pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], ) expect(@irs_attempts_api_tracker).to receive(:idv_gpo_verification_submitted). - with(success: false, failure_reason: otp_code_incorrect) + with(success: false) action @@ -425,7 +424,7 @@ which_letter: nil, letter_count: 1, attempts: 1, - error_details: otp_code_incorrect, + error_details: { otp: { confirmation_code_incorrect: true } }, pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], } expect(@analytics).to receive(:track_event).with( @@ -474,7 +473,7 @@ which_letter: nil, letter_count: 1, attempts: 1, - error_details: otp_code_incorrect, + error_details: { otp: { confirmation_code_incorrect: true } }, pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], ).exactly(max_attempts - 1).times expect(@analytics).to receive(:track_event).with( diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index 7da85f798e8..7f1efc5684a 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -62,7 +62,7 @@ front: ['Please fill in this field.'], }, error_details: { - front: [:blank], + front: { blank: true }, }, user_id: user.uuid, attempts: 1, @@ -123,7 +123,7 @@ front: [I18n.t('doc_auth.errors.not_a_file')], }, error_details: { - front: [I18n.t('doc_auth.errors.not_a_file')], + front: { not_a_file: true }, }, user_id: user.uuid, attempts: 1, @@ -148,7 +148,6 @@ document_issued: nil, document_number: nil, document_state: nil, - failure_reason: { front: ['The selection was not a valid file.'] }, first_name: nil, last_name: nil, success: false }, @@ -261,7 +260,7 @@ limit: [I18n.t('errors.doc_auth.rate_limited_heading')], }, error_details: { - limit: [I18n.t('errors.doc_auth.rate_limited_heading')], + limit: { rate_limited: true }, }, user_id: user.uuid, attempts: IdentityConfig.store.doc_auth_max_attempts, @@ -292,7 +291,6 @@ document_issued: nil, document_number: nil, document_state: nil, - failure_reason: { limit: ['We couldn’t verify your ID'] }, first_name: nil, last_name: nil, success: false }, @@ -429,7 +427,6 @@ expect(@irs_attempts_api_tracker).to receive(:track_event).with( :idv_document_upload_submitted, success: true, - failure_reason: nil, document_back_image_filename: nil, document_front_image_filename: nil, document_image_encryption_key: nil, @@ -458,7 +455,6 @@ :idv_document_upload_submitted, hash_including( success: true, - failure_reason: nil, document_back_image_filename: match(document_filename_regex), document_front_image_filename: match(document_filename_regex), document_image_encryption_key: match(base64_regex), @@ -525,8 +521,6 @@ expect(@irs_attempts_api_tracker).to receive(:track_event).with( :idv_document_upload_submitted, success: false, - failure_reason: { name: - ['We couldn’t read the full name on your ID. Try taking new pictures.'] }, document_state: 'ND', document_number: nil, document_issued: nil, @@ -603,7 +597,7 @@ name: [I18n.t('doc_auth.errors.alerts.full_name_check')], }, error_details: { - name: [I18n.t('doc_auth.errors.alerts.full_name_check')], + name: { name: true }, }, attention_with_barcode: false, user_id: user.uuid, @@ -625,8 +619,6 @@ expect(@irs_attempts_api_tracker).to receive(:track_event).with( :idv_document_upload_submitted, success: false, - failure_reason: { name: - ['We couldn’t read the full name on your ID. Try taking new pictures.'] }, document_state: 'ND', document_number: nil, document_issued: nil, @@ -703,7 +695,7 @@ state: [I18n.t('doc_auth.errors.general.no_liveness')], }, error_details: { - state: [:wrong_length], + state: { wrong_length: true }, }, attention_with_barcode: false, user_id: user.uuid, @@ -725,8 +717,6 @@ expect(@irs_attempts_api_tracker).to receive(:track_event).with( :idv_document_upload_submitted, success: false, - failure_reason: { state: - ['Try taking new pictures.'] }, document_state: 'Maryland', document_number: nil, document_issued: nil, @@ -803,7 +793,7 @@ dob: [I18n.t('doc_auth.errors.alerts.birth_date_checks')], }, error_details: { - dob: [I18n.t('doc_auth.errors.alerts.birth_date_checks')], + dob: { dob: true }, }, attention_with_barcode: false, user_id: user.uuid, @@ -822,8 +812,6 @@ expect(@irs_attempts_api_tracker).to receive(:track_event).with( :idv_document_upload_submitted, success: false, - failure_reason: { dob: - ['We couldn’t read the birth date on your ID. Try taking new pictures.'] }, document_back_image_filename: nil, document_front_image_filename: nil, document_image_encryption_key: nil, diff --git a/spec/controllers/idv/in_person/ssn_controller_spec.rb b/spec/controllers/idv/in_person/ssn_controller_spec.rb index 62002508c24..0e622b3a339 100644 --- a/spec/controllers/idv/in_person/ssn_controller_spec.rb +++ b/spec/controllers/idv/in_person/ssn_controller_spec.rb @@ -75,7 +75,10 @@ irs_reproofing: false, step: 'ssn', same_address_as_id: true, - pii_like_keypaths: [[:same_address_as_id], [:state_id, :state_id_jurisdiction]], + pii_like_keypaths: [ + [:same_address_as_id], + [:proofing_results, :context, :stages, :state_id, :state_id_jurisdiction], + ], }.merge(ab_test_args) end @@ -199,7 +202,7 @@ errors: { ssn: ['Enter a nine-digit Social Security number'], }, - error_details: { ssn: [:invalid] }, + error_details: { ssn: { invalid: true } }, same_address_as_id: true, pii_like_keypaths: [[:same_address_as_id], [:errors, :ssn], [:error_details, :ssn]], }.merge(ab_test_args) diff --git a/spec/controllers/idv/otp_verification_controller_spec.rb b/spec/controllers/idv/otp_verification_controller_spec.rb index 12c31acbcf5..3c542b13646 100644 --- a/spec/controllers/idv/otp_verification_controller_spec.rb +++ b/spec/controllers/idv/otp_verification_controller_spec.rb @@ -170,7 +170,6 @@ expect(@irs_attempts_api_tracker).to receive(:idv_phone_otp_submitted).with( success: true, **phone_property, - failure_reason: {}, ) put :update, params: otp_code_param @@ -183,9 +182,6 @@ expect(@irs_attempts_api_tracker).to receive(:idv_phone_otp_submitted).with( success: false, **phone_property, - failure_reason: { - code_matches: false, - }, ) put :update, params: invalid_otp_code_param @@ -208,9 +204,6 @@ expect(@irs_attempts_api_tracker).to receive(:idv_phone_otp_submitted).with( success: false, **phone_property, - failure_reason: { - code_expired: true, - }, ) put :update, params: otp_code_param diff --git a/spec/controllers/idv/phone_controller_spec.rb b/spec/controllers/idv/phone_controller_spec.rb index bee6b83d0ec..2c0d6a88298 100644 --- a/spec/controllers/idv/phone_controller_spec.rb +++ b/spec/controllers/idv/phone_controller_spec.rb @@ -239,12 +239,6 @@ allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args) end context 'when form is invalid' do - let(:improbable_phone_error) do - { - phone: [:improbable_phone], - otp_delivery_preference: [:inclusion], - } - end let(:improbable_phone_message) { t('errors.messages.improbable_phone') } let(:improbable_otp_message) { 'is not included in the list' } let(:improbable_phone_number) { '703' } @@ -285,7 +279,6 @@ expect(@irs_attempts_api_tracker).to receive(:idv_phone_submitted).with( success: false, phone_number: improbable_phone_number, - failure_reason: improbable_phone_error, ) put :create, params: improbable_phone_form @@ -296,7 +289,10 @@ phone: [improbable_phone_message], otp_delivery_preference: [improbable_otp_message], }, - error_details: improbable_phone_error, + error_details: { + phone: { improbable_phone: true }, + otp_delivery_preference: { inclusion: true }, + }, pii_like_keypaths: [[:errors, :phone], [:error_details, :phone]], country_code: nil, area_code: nil, @@ -329,7 +325,6 @@ expect(@irs_attempts_api_tracker).to receive(:idv_phone_submitted).with( success: true, phone_number: good_phone, - failure_reason: nil, ) phone_params = { diff --git a/spec/controllers/idv/ssn_controller_spec.rb b/spec/controllers/idv/ssn_controller_spec.rb index 5a7921a1086..60a251e7f80 100644 --- a/spec/controllers/idv/ssn_controller_spec.rb +++ b/spec/controllers/idv/ssn_controller_spec.rb @@ -208,7 +208,7 @@ errors: { ssn: [t('idv.errors.pattern_mismatch.ssn')], }, - error_details: { ssn: [:invalid] }, + error_details: { ssn: { invalid: true } }, pii_like_keypaths: [[:same_address_as_id], [:errors, :ssn], [:error_details, :ssn]], }.merge(ab_test_args) end diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index a0225e75013..4bcaa3dae30 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -190,8 +190,6 @@ document_capture_session end - let(:expected_failure_reason) { DocAuthHelper::SAMPLE_TMX_SUMMARY_REASON_CODE } - before do controller. idv_session.verify_info_step_document_capture_session_uuid = document_capture_session.uuid @@ -209,7 +207,6 @@ it 'it logs IRS idv_tmx_fraud_check event' do expect(@irs_attempts_api_tracker).to receive(:idv_tmx_fraud_check).with( success: true, - failure_reason: nil, ) get :show end @@ -226,7 +223,6 @@ it 'it logs IRS idv_tmx_fraud_check event' do expect(@irs_attempts_api_tracker).to receive(:idv_tmx_fraud_check).with( success: false, - failure_reason: expected_failure_reason, ) get :show end @@ -243,7 +239,6 @@ it 'it logs IRS idv_tmx_fraud_check event' do expect(@irs_attempts_api_tracker).to receive(:idv_tmx_fraud_check).with( success: false, - failure_reason: expected_failure_reason, ) get :show end @@ -260,7 +255,6 @@ it 'it logs IRS idv_tmx_fraud_check event' do expect(@irs_attempts_api_tracker).to receive(:idv_tmx_fraud_check).with( success: false, - failure_reason: expected_failure_reason, ) get :show end diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index bbeb92bea08..d55eb8d4ff4 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -765,7 +765,7 @@ def name_id_version(format_urn) analytics_hash = { success: false, errors: { authn_context: [t('errors.messages.unauthorized_authn_context')] }, - error_details: { authn_context: [:unauthorized_authn_context] }, + error_details: { authn_context: { unauthorized_authn_context: true } }, nameid_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT, authn_context: ['http://idmanagement.gov/ns/assurance/loa/5'], authn_context_comparison: 'exact', @@ -984,7 +984,7 @@ def name_id_version(format_urn) analytics_hash = { success: false, errors: { service_provider: [t('errors.messages.unauthorized_service_provider')] }, - error_details: { service_provider: [:unauthorized_service_provider] }, + error_details: { service_provider: { unauthorized_service_provider: true } }, nameid_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT, authn_context: request_authn_contexts, authn_context_comparison: 'exact', @@ -1026,8 +1026,8 @@ def name_id_version(format_urn) authn_context: [t('errors.messages.unauthorized_authn_context')], }, error_details: { - authn_context: [:unauthorized_authn_context], - service_provider: [:unauthorized_service_provider], + authn_context: { unauthorized_authn_context: true }, + service_provider: { unauthorized_service_provider: true }, }, nameid_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT, authn_context: ['http://idmanagement.gov/ns/assurance/loa/5'], @@ -1417,7 +1417,7 @@ def name_id_version(format_urn) analytics_hash = { success: false, errors: { nameid_format: [t('errors.messages.unauthorized_nameid_format')] }, - error_details: { nameid_format: [:unauthorized_nameid_format] }, + error_details: { nameid_format: { unauthorized_nameid_format: true } }, nameid_format: Saml::Idp::Constants::NAME_ID_FORMAT_EMAIL, authn_context: request_authn_contexts, authn_context_comparison: 'exact', diff --git a/spec/controllers/sign_up/email_confirmations_controller_spec.rb b/spec/controllers/sign_up/email_confirmations_controller_spec.rb index 017e7fd7eef..f3dddcaea73 100644 --- a/spec/controllers/sign_up/email_confirmations_controller_spec.rb +++ b/spec/controllers/sign_up/email_confirmations_controller_spec.rb @@ -2,12 +2,10 @@ RSpec.describe SignUp::EmailConfirmationsController do describe '#create' do - let(:token_not_found_error) { { confirmation_token: [:not_found] } } - let(:token_expired_error) { { confirmation_token: [:expired] } } let(:analytics_token_error_hash) do { success: false, - error_details: token_not_found_error, + error_details: { confirmation_token: { not_found: true } }, errors: { confirmation_token: ['not found'] }, user_id: nil, } @@ -16,7 +14,6 @@ { email: nil, success: false, - failure_reason: token_not_found_error, } end @@ -97,7 +94,6 @@ expect(@irs_attempts_api_tracker).to receive(:user_registration_email_confirmation).with( email: email_address.email, success: false, - failure_reason: { email: [:already_confirmed] }, ) get :create, params: { confirmation_token: 'foo' } @@ -117,7 +113,7 @@ analytics_hash = { success: false, errors: { confirmation_token: [t('errors.messages.expired')] }, - error_details: token_expired_error, + error_details: { confirmation_token: { expired: true } }, user_id: email_address.user.uuid, } @@ -127,7 +123,6 @@ expect(@irs_attempts_api_tracker).to receive(:user_registration_email_confirmation).with( email: email_address.email, success: false, - failure_reason: token_expired_error, ) get :create, params: { confirmation_token: 'foo' } @@ -149,7 +144,7 @@ analytics_hash = { success: false, errors: { confirmation_token: [t('errors.messages.expired')] }, - error_details: token_expired_error, + error_details: { confirmation_token: { expired: true } }, user_id: user.uuid, } @@ -159,7 +154,6 @@ expect(@irs_attempts_api_tracker).to receive(:user_registration_email_confirmation).with( email: email_address.email, success: false, - failure_reason: token_expired_error, ) get :create, params: { confirmation_token: 'foo' } @@ -229,7 +223,6 @@ expect(@irs_attempts_api_tracker).to receive(:user_registration_email_confirmation).with( email: email_address.email, success: true, - failure_reason: nil, ) get :create, params: { confirmation_token: 'foo' } diff --git a/spec/controllers/sign_up/passwords_controller_spec.rb b/spec/controllers/sign_up/passwords_controller_spec.rb index bb7a1a1e975..8f48a43c5bc 100644 --- a/spec/controllers/sign_up/passwords_controller_spec.rb +++ b/spec/controllers/sign_up/passwords_controller_spec.rb @@ -16,7 +16,7 @@ end let(:password) { 'NewVal!dPassw0rd' } let(:password_confirmation) { password } - let(:success_properties) { { success: true, failure_reason: nil } } + let(:success_properties) { { success: true } } context 'with valid password' do let!(:user) { create(:user, :unconfirmed, confirmation_token: token) } @@ -61,15 +61,6 @@ context 'with an invalid password' do let!(:user) { create(:user, :unconfirmed, confirmation_token: token) } - let(:analytics_hash) do - { - success: false, - errors: errors, - error_details: error_details, - user_id: user.uuid, - request_id_present: false, - } - end before do stub_analytics @@ -79,70 +70,70 @@ context 'with a password that is too short' do let(:password) { 'NewVal' } let(:password_confirmation) { 'NewVal' } - let(:errors) do - { - password: - [t( - 'errors.attributes.password.too_short', - count: Devise.password_length.first, - )], - password_confirmation: - [I18n.t('errors.messages.too_short', count: Devise.password_length.first)], - } - end - let(:error_details) do - { - password: [:too_short], - password_confirmation: [:too_short], - } - end it 'tracks an invalid password event' do - expect(@analytics).to receive(:track_event). - with( - '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(@irs_attempts_api_tracker).to receive(:user_registration_password_submitted). with( success: false, - failure_reason: error_details, ) expect(@irs_attempts_api_tracker).not_to receive(:user_registration_email_confirmation) subject + + expect(@analytics).to have_logged_event( + 'User Registration: Email Confirmation', + errors: {}, + error_details: nil, + success: true, + user_id: user.uuid, + ) + expect(@analytics).to have_logged_event( + 'Password Creation', + success: false, + errors: { + password: [ + t('errors.attributes.password.too_short', count: Devise.password_length.first), + ], + password_confirmation: [ + t('errors.messages.too_short', count: Devise.password_length.first), + ], + }, + error_details: { + password: { too_short: true }, + password_confirmation: { too_short: true }, + }, + user_id: user.uuid, + request_id_present: false, + ) end end context 'when password confirmation does not match' do let(:password) { 'NewVal!dPassw0rd' } let(:password_confirmation) { 'bad match password' } - let(:errors) do - { - password_confirmation: - [t('errors.messages.password_mismatch')], - } - end - let(:error_details) do - { - password_confirmation: [t('errors.messages.password_mismatch')], - } - end it 'tracks invalid password_confirmation error' do - expect(@analytics).to receive(:track_event). - with( - '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) - subject + + expect(@analytics).to have_logged_event( + 'User Registration: Email Confirmation', + errors: {}, + error_details: nil, + success: true, + user_id: user.uuid, + ) + expect(@analytics).to have_logged_event( + 'Password Creation', + success: false, + errors: { + password_confirmation: [t('errors.messages.password_mismatch')], + }, + error_details: { + password_confirmation: { mismatch: true }, + }, + user_id: user.uuid, + request_id_present: false, + ) end end end diff --git a/spec/controllers/sign_up/registrations_controller_spec.rb b/spec/controllers/sign_up/registrations_controller_spec.rb index 2a6fe362664..33296dd4589 100644 --- a/spec/controllers/sign_up/registrations_controller_spec.rb +++ b/spec/controllers/sign_up/registrations_controller_spec.rb @@ -58,7 +58,7 @@ end describe '#create' do - let(:success_properties) { { success: true, failure_reason: nil } } + let(:success_properties) { { success: true } } context 'when registering with a new email' do it 'tracks successful user registration' do stub_analytics @@ -156,7 +156,7 @@ success: false, rate_limited: false, errors: { email: [t('valid_email.validations.email.invalid')] }, - error_details: { email: [:invalid] }, + error_details: { email: { invalid: true } }, email_already_exists: false, user_id: 'anonymous-uuid', domain_name: 'invalid', @@ -169,7 +169,6 @@ :user_registration_email_submitted, email: 'invalid@', success: false, - failure_reason: { email: [:invalid] }, ) post :create, params: { user: { email: 'invalid@', request_id: '', terms_accepted: '1' } } 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 f62578349b0..71bea1af56f 100644 --- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb @@ -133,7 +133,7 @@ properties = { success: false, - error_details: { code: [:wrong_length, 'incorrect'] }, + error_details: { code: { wrong_length: true, incorrect: true } }, confirmation_for_add_phone: false, context: 'authentication', multi_factor_auth_method: 'sms', @@ -204,7 +204,7 @@ properties = { success: false, - error_details: { code: [:wrong_length, 'incorrect'] }, + error_details: { code: { wrong_length: true, incorrect: true } }, confirmation_for_add_phone: false, context: 'authentication', multi_factor_auth_method: 'sms', @@ -546,7 +546,7 @@ properties = { success: false, errors: nil, - error_details: { code: [:wrong_length, 'incorrect'] }, + error_details: { code: { wrong_length: true, incorrect: true } }, confirmation_for_add_phone: true, context: 'confirmation', multi_factor_auth_method: 'sms', 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 aac99a3f104..36312e154b7 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 @@ -158,7 +158,7 @@ properties = { success: false, errors: { personal_key: [t('errors.messages.personal_key_incorrect')] }, - error_details: { personal_key: [:personal_key_incorrect] }, + error_details: { personal_key: { personal_key_incorrect: true } }, multi_factor_auth_method: 'personal-key', multi_factor_auth_method_created_at: personal_key_generated_at.strftime('%s%L'), } 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 93d2529de60..442f3ca293a 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 @@ -126,7 +126,6 @@ expect(@irs_attempts_api_tracker).to receive(:mfa_login_piv_cac).with( success: true, subject_dn: x509_subject, - failure_reason: nil, ) expect(@analytics).to receive(:track_event). @@ -230,7 +229,6 @@ expect(@irs_attempts_api_tracker).to receive(:mfa_login_piv_cac).with( success: false, subject_dn: bad_dn, - failure_reason: piv_cac_mismatch, ) expect(@analytics).to receive(:track_event). 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 5fd496581a1..2431abb0291 100644 --- a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb @@ -223,7 +223,7 @@ result = { context: 'authentication', multi_factor_auth_method: 'webauthn', success: false, - error_details: { authenticator_data: ['invalid_authenticator_data'] }, + error_details: { authenticator_data: { invalid_authenticator_data: true } }, webauthn_configuration_id: webauthn_configuration.id, multi_factor_auth_method_created_at: webauthn_configuration.created_at. strftime('%s%L') } @@ -280,17 +280,18 @@ expect(@analytics).to receive(:track_mfa_submit_event).with( success: false, error_details: { - authenticator_data: [:blank], - client_data_json: [:blank], - signature: [:blank], - webauthn_configuration: [:blank], - webauthn_error: [webauthn_error], + authenticator_data: { blank: true }, + client_data_json: { blank: true }, + signature: { blank: true }, + webauthn_configuration: { blank: true }, + webauthn_error: { present: true }, }, context: UserSessionContext::AUTHENTICATION_CONTEXT, multi_factor_auth_method: 'webauthn_platform', multi_factor_auth_method_created_at: second_webauthn_platform_configuration.created_at.strftime('%s%L'), webauthn_configuration_id: nil, + frontend_error: 'NotAllowedError', ) patch :confirm, params: params diff --git a/spec/controllers/users/edit_phone_controller_spec.rb b/spec/controllers/users/edit_phone_controller_spec.rb index fb460dfbd84..c9df63102fd 100644 --- a/spec/controllers/users/edit_phone_controller_spec.rb +++ b/spec/controllers/users/edit_phone_controller_spec.rb @@ -38,7 +38,7 @@ attributes = { success: false, errors: hash_including(:delivery_preference), - error_details: { delivery_preference: [:inclusion] }, + error_details: { delivery_preference: { inclusion: true } }, delivery_preference: 'noise', make_default_number: true, phone_configuration_id: phone_configuration.id, diff --git a/spec/controllers/users/passwords_controller_spec.rb b/spec/controllers/users/passwords_controller_spec.rb index 40eb505c572..ef5e2ae8fbb 100644 --- a/spec/controllers/users/passwords_controller_spec.rb +++ b/spec/controllers/users/passwords_controller_spec.rb @@ -22,7 +22,7 @@ allow(@analytics).to receive(:track_event) expect(@irs_attempts_api_tracker).to receive(:logged_in_password_change). - with(failure_reason: nil, success: true) + with(success: true) params = { password: 'salty new password', @@ -48,6 +48,7 @@ stub_sign_in(user) Pii::Cacher.new(user, controller.user_session).save_decrypted_pii( Pii::Attributes.new(ssn: '111-222-3333'), + user.active_profile.id, ) params = { @@ -132,14 +133,8 @@ end it 'renders edit' do - password_short_error = { - password: [:too_short], - password_confirmation: [:too_short], - } - expect(@irs_attempts_api_tracker).to receive(:logged_in_password_change).with( success: false, - failure_reason: password_short_error, ) patch :update, params: { update_user_password_form: params } @@ -159,7 +154,10 @@ count: Devise.password_length.first, )], }, - error_details: password_short_error, + error_details: { + password: { too_short: true }, + password_confirmation: { too_short: true }, + }, pending_profile_present: false, active_profile_present: false, user_id: subject.current_user.uuid, @@ -189,9 +187,6 @@ it 'renders edit' do expect(@irs_attempts_api_tracker).to receive(:logged_in_password_change).with( success: false, - failure_reason: { - password_confirmation: [t('errors.messages.password_mismatch')], - }, ) patch :update, params: { update_user_password_form: params } @@ -203,7 +198,7 @@ password_confirmation: [t('errors.messages.password_mismatch')], }, error_details: { - password_confirmation: [t('errors.messages.password_mismatch')], + password_confirmation: { mismatch: true }, }, pending_profile_present: false, active_profile_present: false, diff --git a/spec/controllers/users/phone_setup_controller_spec.rb b/spec/controllers/users/phone_setup_controller_spec.rb index 43334bbbfe5..b898b40c7b5 100644 --- a/spec/controllers/users/phone_setup_controller_spec.rb +++ b/spec/controllers/users/phone_setup_controller_spec.rb @@ -70,10 +70,10 @@ ], }, error_details: { - phone: [ - :improbable_phone, - t('two_factor_authentication.otp_delivery_preference.voice_unsupported', location: ''), - ], + phone: { + improbable_phone: true, + voice_unsupported: true, + }, }, otp_delivery_preference: 'sms', area_code: nil, diff --git a/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb b/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb index 7151f319f8e..4c294a70f4c 100644 --- a/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb @@ -132,7 +132,6 @@ :mfa_enroll_piv_cac, success: true, subject_dn: 'some dn', - failure_reason: nil, ) get :new, params: { token: good_token } @@ -156,7 +155,6 @@ :mfa_enroll_piv_cac, success: true, subject_dn: 'some dn', - failure_reason: nil, ) get :new, params: { token: good_token } @@ -172,7 +170,6 @@ :mfa_enroll_piv_cac, success: true, subject_dn: 'some dn', - failure_reason: nil, ) get :new, params: { token: good_token } @@ -185,7 +182,6 @@ :mfa_enroll_piv_cac, success: true, subject_dn: 'some dn', - failure_reason: nil, ) get :new, params: { token: good_token } @@ -213,7 +209,6 @@ :mfa_enroll_piv_cac, success: false, subject_dn: nil, - failure_reason: { type: 'certificate.bad' }, ) get :new, params: { token: bad_token } diff --git a/spec/controllers/users/reset_passwords_controller_spec.rb b/spec/controllers/users/reset_passwords_controller_spec.rb index 09c557774cf..c77a019caf3 100644 --- a/spec/controllers/users/reset_passwords_controller_spec.rb +++ b/spec/controllers/users/reset_passwords_controller_spec.rb @@ -4,8 +4,7 @@ let(:password_error_message) do t('errors.attributes.password.too_short.other', count: Devise.password_length.first) end - let(:success_properties) { { success: true, failure_reason: nil } } - let(:token_expired_error) { 'token_expired' } + let(:success_properties) { { success: true } } describe '#edit' do let(:user) { instance_double('User', uuid: '123') } let(:email_address) { instance_double('EmailAddress') } @@ -24,7 +23,6 @@ end context 'no user matches token' do - let(:user_blank_error) { { user: [:blank] } } let(:token) { 'foo' } before do session[:reset_password_token] = token @@ -33,7 +31,7 @@ { success: false, errors: { user: ['invalid_token'] }, - error_details: user_blank_error, + error_details: { user: { blank: true } }, user_id: nil, } end @@ -41,7 +39,6 @@ it 'redirects to page where user enters email for password reset token' do expect(@irs_attempts_api_tracker).to receive(:forgot_password_email_confirmed).with( success: false, - failure_reason: user_blank_error, ) get :edit @@ -54,7 +51,6 @@ end context 'token expired' do - let(:user_token_error) { { user: [token_expired_error] } } let(:token) { 'foo' } before do session[:reset_password_token] = token @@ -62,8 +58,8 @@ let(:analytics_hash) do { success: false, - errors: user_token_error, - error_details: user_token_error, + errors: { user: ['token_expired'] }, + error_details: { user: { token_expired: true } }, user_id: '123', } end @@ -76,12 +72,11 @@ end context 'no user matches token' do - let(:user_blank_error) { { user: [:blank] } } let(:analytics_hash) do { success: false, errors: { user: ['invalid_token'] }, - error_details: user_blank_error, + error_details: { user: { blank: true } }, user_id: nil, } end @@ -93,7 +88,6 @@ it 'redirects to page where user enters email for password reset token' do expect(@irs_attempts_api_tracker).to receive(:forgot_password_email_confirmed).with( success: false, - failure_reason: user_blank_error, ) get :edit @@ -106,12 +100,11 @@ end context 'token expired' do - let(:user_token_error) { { user: [token_expired_error] } } let(:analytics_hash) do { success: false, - errors: user_token_error, - error_details: user_token_error, + errors: { user: ['token_expired'] }, + error_details: { user: { token_expired: true } }, user_id: '123', } end @@ -125,7 +118,6 @@ it 'redirects to page where user enters email for password reset token' do expect(@irs_attempts_api_tracker).to receive(:forgot_password_email_confirmed).with( success: false, - failure_reason: user_token_error, ) get :edit @@ -192,18 +184,7 @@ end describe '#update' do - let(:password_short_error) { { password: [:too_short] } } - - let(:password_token_error) { { reset_password_token: [token_expired_error] } } context 'user submits new password after token expires' do - let(:reset_password_error_details) do - { - **password_short_error, - password_confirmation: [:too_short], - **password_token_error, - } - end - it 'redirects to page where user enters email for password reset token' do stub_analytics stub_attempts_tracker @@ -211,7 +192,6 @@ expect(@irs_attempts_api_tracker).to receive(:forgot_password_new_password_submitted).with( success: false, - failure_reason: reset_password_error_details, ) raw_reset_token, db_confirmation_token = @@ -240,9 +220,13 @@ 'errors.messages.too_short.other', count: Devise.password_length.first, )], - **password_token_error, + reset_password_token: ['token_expired'], + }, + error_details: { + password: { too_short: true }, + password_confirmation: { too_short: true }, + reset_password_token: { token_expired: true }, }, - error_details: reset_password_error_details, user_id: user.uuid, profile_deactivated: false, pending_profile_invalidated: false, @@ -287,8 +271,8 @@ )], }, error_details: { - password: [:too_short], - password_confirmation: [:too_short], + password: { too_short: true }, + password_confirmation: { too_short: true }, }, user_id: user.uuid, profile_deactivated: false, @@ -300,10 +284,6 @@ with('Password Reset: Password Submitted', analytics_hash) expect(@irs_attempts_api_tracker).to receive(:forgot_password_new_password_submitted).with( success: false, - failure_reason: { - password: [:too_short], - password_confirmation: [:too_short], - }, ) put :update, params: { reset_password_form: form_params } @@ -340,7 +320,7 @@ password_confirmation: [t('errors.messages.password_mismatch')], }, error_details: { - password_confirmation: [t('errors.messages.password_mismatch')], + password_confirmation: { mismatch: true }, }, user_id: user.uuid, profile_deactivated: false, @@ -352,9 +332,6 @@ with('Password Reset: Password Submitted', analytics_hash) expect(@irs_attempts_api_tracker).to receive(:forgot_password_new_password_submitted).with( success: false, - failure_reason: { - password_confirmation: [t('errors.messages.password_mismatch')], - }, ) put :update, params: { reset_password_form: form_params } @@ -709,7 +686,7 @@ analytics_hash = { success: false, errors: { email: [t('valid_email.validations.email.invalid')] }, - error_details: { email: [:invalid] }, + error_details: { email: { invalid: true } }, user_id: 'nonexistent-uuid', confirmed: false, active_profile: false, diff --git a/spec/controllers/users/totp_setup_controller_spec.rb b/spec/controllers/users/totp_setup_controller_spec.rb index be785f60bd8..e0ef5196936 100644 --- a/spec/controllers/users/totp_setup_controller_spec.rb +++ b/spec/controllers/users/totp_setup_controller_spec.rb @@ -224,7 +224,7 @@ result = { success: false, - error_details: { name: [:blank] }, + error_details: { name: { blank: true } }, errors: { name: [t('errors.messages.blank')] }, totp_secret_present: true, multi_factor_auth_method: 'totp', diff --git a/spec/controllers/users/two_factor_authentication_controller_spec.rb b/spec/controllers/users/two_factor_authentication_controller_spec.rb index 8fc08267900..622e104260f 100644 --- a/spec/controllers/users/two_factor_authentication_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_controller_spec.rb @@ -274,9 +274,7 @@ def index let(:default_parameters) do { **valid_phone_number, otp_delivery_method: 'sms' } end - let(:success_parameters) do - { success: true, **default_parameters, failure_reason: nil } - end + let(:success_parameters) { { success: true, **default_parameters } } before do @user = create(:user, :with_phone) @@ -402,9 +400,7 @@ def index stub_attempts_tracker expect(@irs_attempts_api_tracker).to receive(:mfa_login_phone_otp_sent). - with(reauthentication: false, **default_parameters, success: false, failure_reason: { - telephony: 'Telephony::OptOutError - Telephony::OptOutError', - }) + with(reauthentication: false, **default_parameters, success: false) get :send_code, params: otp_delivery_form_sms end diff --git a/spec/controllers/users/verify_personal_key_controller_spec.rb b/spec/controllers/users/verify_personal_key_controller_spec.rb index 4a71520eb0a..e852fcc0c08 100644 --- a/spec/controllers/users/verify_personal_key_controller_spec.rb +++ b/spec/controllers/users/verify_personal_key_controller_spec.rb @@ -87,8 +87,14 @@ let(:error_text) { 'Incorrect personal key' } let(:personal_key_bad_params) { { personal_key: 'baaad' } } let(:personal_key_error) { { personal_key: [error_text] } } - let(:failure_properties) { { success: false, failure_reason: personal_key_error } } - let(:pii_like_keypaths_errors) { [[:errors, :personal_key], [:error_details, :personal_key]] } + let(:failure_properties) { { success: false } } + let(:pii_like_keypaths_errors) do + [ + [:errors, :personal_key], + [:error_details, :personal_key], + [:error_details, :personal_key, :personal_key], + ] + end let(:response_ok) { FormResponse.new(success: true, errors: {}) } let(:response_bad) { FormResponse.new(success: false, errors: personal_key_error, extra: {}) } @@ -121,7 +127,6 @@ stub_attempts_tracker expect(@irs_attempts_api_tracker).to receive(:personal_key_reactivation_submitted).with( - failure_reason: nil, success: true, ).once @@ -163,7 +168,7 @@ expect(@analytics).to receive(:track_event).with( 'Personal key reactivation: Personal key form submitted', errors: { personal_key: ['Please fill in this field.', error_text] }, - error_details: { personal_key: [:blank, :personal_key_incorrect] }, + error_details: { personal_key: { blank: true, personal_key: true } }, success: false, pii_like_keypaths: pii_like_keypaths_errors, ).once diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb index 511b99d99b9..96758345f05 100644 --- a/spec/controllers/users/webauthn_setup_controller_spec.rb +++ b/spec/controllers/users/webauthn_setup_controller_spec.rb @@ -230,6 +230,18 @@ expect(additional_mfa_check).to be_truthy end end + + context 'when the back button is clicked after platform is added' do + let(:user) { create(:user, :with_webauthn_platform) } + before do + controller.user_session[:in_account_creation_flow] = true + end + it 'should redirect to authentication methods setup' do + get :new, params: { platform: true } + + expect(response).to redirect_to(authentication_methods_setup_path) + end + end end describe 'multiple MFA handling' do @@ -392,10 +404,7 @@ '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, - )] }, + error_details: { name: { attestation_error: true } }, in_account_creation_flow: false, mfa_method_counts: {}, multi_factor_auth_method: 'webauthn_platform', diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index 90d49d3d036..744fa2bbc33 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -63,10 +63,10 @@ flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'Frontend: IdV: front image added' => { - width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: 'AUTO', fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil + width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil }, 'Frontend: IdV: back image added' => { - width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: 'AUTO', fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil + width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil }, 'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil @@ -171,10 +171,10 @@ flow_path: 'hybrid', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'Frontend: IdV: front image added' => { - width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'hybrid', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: 'AUTO', fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil, phone_with_camera: nil + width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'hybrid', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil, phone_with_camera: nil }, 'Frontend: IdV: back image added' => { - width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'hybrid', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: 'AUTO', fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil, phone_with_camera: nil + width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'hybrid', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil, phone_with_camera: nil }, 'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'hybrid', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil @@ -276,10 +276,10 @@ flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'Frontend: IdV: front image added' => { - width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: 'AUTO', fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil + width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil }, 'Frontend: IdV: back image added' => { - width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: 'AUTO', fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil + width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil }, 'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil @@ -363,10 +363,10 @@ flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'Frontend: IdV: front image added' => { - width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: 'AUTO', fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil + width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil }, 'Frontend: IdV: back image added' => { - width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: 'AUTO', fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil + width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil }, 'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb index c2fa0f44f69..21471799825 100644 --- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb +++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb @@ -52,7 +52,6 @@ ).with( success: true, phone_number: '+1 415-555-0199', - failure_reason: nil, ) expect(Telephony).to receive(:send_doc_auth_link). @@ -92,7 +91,6 @@ expect(fake_attempts_tracker).to receive(:idv_phone_upload_link_sent).with( success: false, phone_number: '+1 225-555-1000', - failure_reason: { telephony: ['TelephonyError'] }, ) fill_in :doc_auth_phone, with: '225-555-1000' diff --git a/spec/features/idv/doc_auth/verify_info_step_spec.rb b/spec/features/idv/doc_auth/verify_info_step_spec.rb index 512407f2d8b..182f6544011 100644 --- a/spec/features/idv/doc_auth/verify_info_step_spec.rb +++ b/spec/features/idv/doc_auth/verify_info_step_spec.rb @@ -86,7 +86,6 @@ it 'logs analytics and attempts tracker events on submit' do expect(fake_attempts_tracker).to receive(:idv_verification_submitted).with( success: true, - failure_reason: nil, **fake_pii_details, ssn: DocAuthHelper::GOOD_SSN, ) @@ -102,7 +101,6 @@ it 'does not proceed to the next page if resolution fails' do expect(fake_attempts_tracker).to receive(:idv_verification_submitted).with( success: false, - failure_reason: { ssn: ['Unverified SSN.'] }, **fake_pii_details, ssn: DocAuthHelper::SSN_THAT_FAILS_RESOLUTION, ) @@ -120,7 +118,6 @@ it 'does not proceed to the next page if resolution raises an exception' do expect(fake_attempts_tracker).to receive(:idv_verification_submitted).with( success: false, - failure_reason: nil, **fake_pii_details, ssn: DocAuthHelper::SSN_THAT_RAISES_EXCEPTION, ) @@ -371,7 +368,6 @@ it 'tracks attempts tracker event with failure reason' do expect(fake_attempts_tracker).to receive(:idv_verification_submitted).with( success: false, - failure_reason: { idv_verification: [:timeout] }, **fake_pii_details, ssn: DocAuthHelper::GOOD_SSN, ) diff --git a/spec/features/openid_connect/authorization_confirmation_spec.rb b/spec/features/openid_connect/authorization_confirmation_spec.rb index 308ed93dade..86e3ec3123f 100644 --- a/spec/features/openid_connect/authorization_confirmation_spec.rb +++ b/spec/features/openid_connect/authorization_confirmation_spec.rb @@ -95,10 +95,8 @@ def create_user_and_remember_device perform_in_browser(:two) do confirm_email_in_a_different_browser(email) expect(current_path).to eq sign_up_completed_path - within('.requested-attributes') do - expect(page).to have_content t('help_text.requested_attributes.email') - expect(page).to have_content email - end + expect(page).to have_content t('help_text.requested_attributes.email') + expect(page).to have_content email click_agree_and_continue diff --git a/spec/features/saml/authorization_confirmation_spec.rb b/spec/features/saml/authorization_confirmation_spec.rb index 269be1ebd93..a9e075f2ad6 100644 --- a/spec/features/saml/authorization_confirmation_spec.rb +++ b/spec/features/saml/authorization_confirmation_spec.rb @@ -107,10 +107,8 @@ def create_user_and_remember_device perform_in_browser(:two) do confirm_email_in_a_different_browser(email) expect(current_path).to eq sign_up_completed_path - within('.requested-attributes') do - expect(page).to have_content t('help_text.requested_attributes.email') - expect(page).to have_content email - end + expect(page).to have_content t('help_text.requested_attributes.email') + expect(page).to have_content email click_agree_and_continue diff --git a/spec/features/saml/ial1_sso_spec.rb b/spec/features/saml/ial1_sso_spec.rb index af747adb3c8..e2ca596dfe9 100644 --- a/spec/features/saml/ial1_sso_spec.rb +++ b/spec/features/saml/ial1_sso_spec.rb @@ -16,15 +16,12 @@ perform_in_browser(:two) do confirm_email_in_a_different_browser(email) expect(current_path).to eq sign_up_completed_path - within('.requested-attributes') do - expect(page).to have_content t('help_text.requested_attributes.email') - expect(page).to have_content email - expect(page).to_not have_content t('help_text.requested_attributes.address') - expect(page).to_not have_content t('help_text.requested_attributes.birthdate') - expect(page).to_not have_content t('help_text.requested_attributes.phone') - expect(page). - to_not have_content t('help_text.requested_attributes.social_security_number') - end + expect(page).to have_content t('help_text.requested_attributes.email') + expect(page).to have_content email + expect(page).to_not have_content t('help_text.requested_attributes.address') + expect(page).to_not have_content t('help_text.requested_attributes.birthdate') + expect(page).to_not have_content t('help_text.requested_attributes.phone') + expect(page).to_not have_content t('help_text.requested_attributes.social_security_number') click_agree_and_continue diff --git a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_no_liveness.json b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_no_liveness.json index 3d925a0543d..d47c7b52a28 100644 --- a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_no_liveness.json +++ b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_no_liveness.json @@ -37,6 +37,11 @@ "Name": "DocIssuerName", "Values": [{"Value": "Connecticut"}] }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssuerType", + "Values": [{"Value": "StateProvince"}] + }, { "Group": "AUTHENTICATION_RESULT", "Name": "DocClassCode", diff --git a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_no_liveness_low_dpi.json b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_no_liveness_low_dpi.json index 216baa127ab..040084be529 100644 --- a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_no_liveness_low_dpi.json +++ b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_no_liveness_low_dpi.json @@ -37,6 +37,11 @@ "Name": "DocIssuerName", "Values": [{"Value": "Connecticut"}] }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssuerType", + "Values": [{"Value": "StateProvince"}] + }, { "Group": "AUTHENTICATION_RESULT", "Name": "DocClassCode", diff --git a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_with_all_failures.json b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_with_all_failures.json index 125d1a27384..3d3282e16f2 100644 --- a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_with_all_failures.json +++ b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_with_all_failures.json @@ -37,6 +37,11 @@ "Name": "DocIssuerName", "Values": [{"Value": "Connecticut"}] }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssuerType", + "Values": [{"Value": "StateProvince"}] + }, { "Group": "AUTHENTICATION_RESULT", "Name": "DocClassCode", diff --git a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_with_liveness.json b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_with_liveness.json index 79c376efeb8..9ae6ac15f2c 100644 --- a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_with_liveness.json +++ b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_failure_with_liveness.json @@ -37,6 +37,11 @@ "Name": "DocIssuerName", "Values": [{"Value": "Connecticut"}] }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssuerType", + "Values": [{"Value": "StateProvince"}] + }, { "Group": "AUTHENTICATION_RESULT", "Name": "DocClassCode", diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index 91b6f680cbf..b92875d83b5 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -89,7 +89,6 @@ document_issued: '2019-12-31', document_number: '1111111111111', document_state: 'MT', - failure_reason: nil, first_name: 'FAKEY', last_name: 'MCFAKERSON', success: true, diff --git a/spec/forms/idv/phone_form_spec.rb b/spec/forms/idv/phone_form_spec.rb index 356d44dc48d..46d89945415 100644 --- a/spec/forms/idv/phone_form_spec.rb +++ b/spec/forms/idv/phone_form_spec.rb @@ -93,16 +93,7 @@ unregistered_phone = '+400258567234' result = subject.submit(phone: unregistered_phone) expect(result.success?).to eq false - expect(result.to_h).to include( - { - error_details: { - phone: [t( - 'two_factor_authentication.otp_delivery_preference.sms_unsupported', - location: 'Romania', - )], - }, - }, - ) + expect(result.to_h).to include(error_details: { phone: { sms_unsupported: true } }) end end diff --git a/spec/forms/openid_connect_authorize_form_spec.rb b/spec/forms/openid_connect_authorize_form_spec.rb index dce7824be51..74395927062 100644 --- a/spec/forms/openid_connect_authorize_form_spec.rb +++ b/spec/forms/openid_connect_authorize_form_spec.rb @@ -64,7 +64,7 @@ expect(result.to_h).to eq( success: false, errors: { response_type: ['is not included in the list'] }, - error_details: { response_type: [:inclusion] }, + error_details: { response_type: { inclusion: true } }, client_id: client_id, prompt: 'select_account', allow_prompt_login: true, diff --git a/spec/forms/otp_verification_form_spec.rb b/spec/forms/otp_verification_form_spec.rb index c4ec4fdd12c..32af977d9ff 100644 --- a/spec/forms/otp_verification_form_spec.rb +++ b/spec/forms/otp_verification_form_spec.rb @@ -45,7 +45,7 @@ expect(result.to_h).to eq( success: false, error_details: { - code: [:blank, :wrong_length], + code: { blank: true, wrong_length: true }, }, multi_factor_auth_method: 'otp_code', multi_factor_auth_method_created_at: phone_configuration.created_at.strftime('%s%L'), @@ -67,7 +67,7 @@ expect(result.to_h).to eq( success: false, error_details: { - code: ['user_otp_missing'], + code: { user_otp_missing: true }, }, multi_factor_auth_method: 'otp_code', multi_factor_auth_method_created_at: phone_configuration.created_at.strftime('%s%L'), @@ -89,7 +89,7 @@ expect(result.to_h).to eq( success: false, error_details: { - code: [:wrong_length, 'incorrect'], + code: { wrong_length: true, incorrect: true }, }, multi_factor_auth_method: 'otp_code', multi_factor_auth_method_created_at: phone_configuration.created_at.strftime('%s%L'), @@ -111,7 +111,7 @@ expect(result.to_h).to eq( success: false, error_details: { - code: ['pattern_mismatch', 'incorrect'], + code: { pattern_mismatch: true, incorrect: true }, }, multi_factor_auth_method: 'otp_code', multi_factor_auth_method_created_at: phone_configuration.created_at.strftime('%s%L'), @@ -136,7 +136,7 @@ expect(result.to_h).to eq( success: false, error_details: { - code: ['user_otp_expired'], + code: { user_otp_expired: true }, }, multi_factor_auth_method: 'otp_code', multi_factor_auth_method_created_at: phone_configuration.created_at.strftime('%s%L'), diff --git a/spec/forms/totp_setup_form_spec.rb b/spec/forms/totp_setup_form_spec.rb index 79a814512d9..6e2dcd7ac8f 100644 --- a/spec/forms/totp_setup_form_spec.rb +++ b/spec/forms/totp_setup_form_spec.rb @@ -80,7 +80,7 @@ expect(form.submit.to_h).to include( success: false, - error_details: { name: [:blank] }, + error_details: { name: { blank: true } }, errors: { name: [t('errors.messages.blank')] }, ) expect(user.auth_app_configurations.any?).to eq false @@ -95,7 +95,7 @@ expect(form2.submit.to_h).to include( success: false, - error_details: { name: [t('errors.piv_cac_setup.unique_name')] }, + error_details: { name: { unique_name: true } }, errors: { name: [t('errors.piv_cac_setup.unique_name')] }, ) end diff --git a/spec/forms/update_user_password_form_spec.rb b/spec/forms/update_user_password_form_spec.rb index f3222eb6738..6778e84fd35 100644 --- a/spec/forms/update_user_password_form_spec.rb +++ b/spec/forms/update_user_password_form_spec.rb @@ -34,7 +34,7 @@ } expect(UpdateUser).not_to receive(:new) - expect(ActiveProfileEncryptor).not_to receive(:new) + expect(UserProfilesEncryptor).not_to receive(:new) expect(subject.submit(params).to_h).to include( success: false, errors: errors, @@ -70,17 +70,21 @@ context 'when the user has an active profile' do let(:profile) { create(:profile, :active, :verified, pii: { ssn: '1234' }) } let(:user) { profile.user } - let(:user_session) { { decrypted_pii: { ssn: '1234' }.to_json } } + let(:user_session) { {} } + + before do + Pii::Cacher.new(user, user_session).save_decrypted_pii({ ssn: '1234' }, profile.id) + end it 'encrypts the active profile' do - encryptor = instance_double(ActiveProfileEncryptor) - allow(ActiveProfileEncryptor).to receive(:new). - with(user, user_session, password).and_return(encryptor) - allow(encryptor).to receive(:call) + encryptor = instance_double(UserProfilesEncryptor) + allow(UserProfilesEncryptor).to receive(:new). + with(user: user, user_session: user_session, password: password).and_return(encryptor) + allow(encryptor).to receive(:encrypt) subject.submit(params) - expect(encryptor).to have_received(:call) + expect(encryptor).to have_received(:encrypt) end it 'logs that the user has an active profile' do @@ -96,11 +100,21 @@ context 'the user has a pending profile' do let(:profile) { create(:profile, :verify_by_mail_pending, :verified, pii: { ssn: '1234' }) } let(:user) { profile.user } + let(:user_session) { {} } - it 'does not call ActiveProfileEncryptor' do - expect(ActiveProfileEncryptor).to_not receive(:new) + before do + Pii::Cacher.new(user, user_session).save_decrypted_pii({ ssn: '1234' }, profile.id) + end + + it 'encrypts the pending profile' do + encryptor = instance_double(UserProfilesEncryptor) + allow(UserProfilesEncryptor).to receive(:new). + with(user: user, user_session: user_session, password: password).and_return(encryptor) + allow(encryptor).to receive(:encrypt) subject.submit(params) + + expect(encryptor).to have_received(:encrypt) end it 'logs that the user has a pending profile' do @@ -114,8 +128,8 @@ end context 'when the user does not have a profile' do - it 'does not call ActiveProfileEncryptor' do - expect(ActiveProfileEncryptor).to_not receive(:new) + it 'does not call UserProfilesEncryptor' do + expect(UserProfilesEncryptor).to_not receive(:new) subject.submit(params) end diff --git a/spec/forms/webauthn_setup_form_spec.rb b/spec/forms/webauthn_setup_form_spec.rb index 235307a57b9..e5207d3e6c7 100644 --- a/spec/forms/webauthn_setup_form_spec.rb +++ b/spec/forms/webauthn_setup_form_spec.rb @@ -215,10 +215,7 @@ 'errors.webauthn_setup.attestation_error', link: MarketingSite.contact_url, )] }, - error_details: { name: [I18n.t( - 'errors.webauthn_setup.attestation_error', - link: MarketingSite.contact_url, - )] }, + error_details: { name: { attestation_error: true } }, **extra_attributes, ) end diff --git a/spec/forms/webauthn_verification_form_spec.rb b/spec/forms/webauthn_verification_form_spec.rb index 2843d0cd3c4..efd4cd2ca1e 100644 --- a/spec/forms/webauthn_verification_form_spec.rb +++ b/spec/forms/webauthn_verification_form_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' RSpec.describe WebauthnVerificationForm do + include Rails.application.routes.url_helpers include WebAuthnHelper let(:user) { create(:user) } @@ -22,6 +23,8 @@ subject(:form) do WebauthnVerificationForm.new( user: user, + platform_authenticator:, + url_options: {}, challenge: challenge, protocol: protocol, authenticator_data: authenticator_data, @@ -59,22 +62,21 @@ ) end end - end - context 'when the input is invalid' do - context 'when user is missing' do - let(:user) { nil } + context 'with client-side webauthn error as blank string' do + let(:webauthn_error) { '' } - it 'returns unsuccessful result' do + it 'returns successful result excluding frontend_error' do expect(result.to_h).to eq( - success: false, - error_details: { user: [:blank], webauthn_configuration: [:blank] }, + success: true, multi_factor_auth_method: 'webauthn', - webauthn_configuration_id: nil, + webauthn_configuration_id: webauthn_configuration.id, ) end end + end + context 'when the input is invalid' do context 'when challenge is missing' do let(:challenge) { nil } @@ -82,8 +84,8 @@ expect(result.to_h).to eq( success: false, error_details: { - challenge: [:blank], - authenticator_data: ['invalid_authenticator_data'], + challenge: { blank: true }, + authenticator_data: { invalid_authenticator_data: true }, }, multi_factor_auth_method: 'webauthn', webauthn_configuration_id: webauthn_configuration.id, @@ -98,7 +100,7 @@ expect(result.to_h).to eq( success: false, error_details: { - authenticator_data: [:blank, 'invalid_authenticator_data'], + authenticator_data: { blank: true, invalid_authenticator_data: true }, }, multi_factor_auth_method: 'webauthn', webauthn_configuration_id: webauthn_configuration.id, @@ -113,8 +115,8 @@ expect(result.to_h).to eq( success: false, error_details: { - client_data_json: [:blank], - authenticator_data: ['invalid_authenticator_data'], + client_data_json: { blank: true }, + authenticator_data: { invalid_authenticator_data: true }, }, multi_factor_auth_method: 'webauthn', webauthn_configuration_id: webauthn_configuration.id, @@ -129,8 +131,8 @@ expect(result.to_h).to eq( success: false, error_details: { - signature: [:blank], - authenticator_data: ['invalid_authenticator_data'], + signature: { blank: true }, + authenticator_data: { invalid_authenticator_data: true }, }, multi_factor_auth_method: 'webauthn', webauthn_configuration_id: webauthn_configuration.id, @@ -144,22 +146,22 @@ it 'returns unsuccessful result' do expect(result.to_h).to eq( success: false, - error_details: { webauthn_configuration: [:blank] }, + error_details: { webauthn_configuration: { blank: true } }, multi_factor_auth_method: 'webauthn', - webauthn_configuration_id: nil, ) end end context 'when a client-side webauthn error is present' do - let(:webauthn_error) { 'Unexpected error!' } + let(:webauthn_error) { 'NotAllowedError' } it 'returns unsuccessful result including client-side webauthn error text' do expect(result.to_h).to eq( success: false, - error_details: { webauthn_error: [webauthn_error] }, + error_details: { webauthn_error: { present: true } }, multi_factor_auth_method: 'webauthn', webauthn_configuration_id: webauthn_configuration.id, + frontend_error: webauthn_error, ) end end @@ -172,7 +174,7 @@ it 'returns unsuccessful result' do expect(result.to_h).to eq( success: false, - error_details: { authenticator_data: ['invalid_authenticator_data'] }, + error_details: { authenticator_data: { invalid_authenticator_data: true } }, multi_factor_auth_method: 'webauthn', webauthn_configuration_id: webauthn_configuration.id, ) @@ -188,7 +190,7 @@ it 'returns unsucessful result' do expect(result.to_h).to eq( success: false, - error_details: { authenticator_data: ['invalid_authenticator_data'] }, + error_details: { authenticator_data: { invalid_authenticator_data: true } }, multi_factor_auth_method: 'webauthn', webauthn_configuration_id: webauthn_configuration.id, ) diff --git a/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx b/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx index d3a46c94e1b..28fd65c4487 100644 --- a/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx +++ b/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx @@ -1188,7 +1188,7 @@ describe('document-capture/components/acuant-capture', () => { mimeType: 'image/jpeg', size: sinon.match.number, attempt: sinon.match.number, - acuantCaptureMode: 'AUTO', + acuantCaptureMode: null, }), ); }); diff --git a/spec/javascript/packs/puerto-rico-guidance-spec.js b/spec/javascript/packs/puerto-rico-guidance-spec.js deleted file mode 100644 index 3760eac59bd..00000000000 --- a/spec/javascript/packs/puerto-rico-guidance-spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import { showOrHidePuertoRicoExtras } from '../../../app/javascript/packs/puerto-rico-guidance'; - -describe('puerto-rico-guidance', () => { - describe('showOrHidePuertoRicoExtras', () => { - beforeEach(() => { - document.body.innerHTML = ` -
    -
    - -
    - -
    - `; - }); - - it('includes class display-none if state is not PR', () => { - const forStateCode = 'NY'; - showOrHidePuertoRicoExtras(forStateCode); - const prExtrasElemClassList = document.querySelector('.puerto-rico-extras')?.classList; - - expect(prExtrasElemClassList).to.contain(['puerto-rico-extras', 'display-none']); - }); - - it('does not include class display-none if state is PR', () => { - const forStateCode = 'PR'; - showOrHidePuertoRicoExtras(forStateCode); - const prExtrasElemClassList = document.querySelector('.puerto-rico-extras')?.classList; - - expect(prExtrasElemClassList).to.contain(['puerto-rico-extras']); - }); - }); -}); diff --git a/spec/javascript/packs/state-guidance-spec.js b/spec/javascript/packs/state-guidance-spec.js new file mode 100644 index 00000000000..6314f2a7f3d --- /dev/null +++ b/spec/javascript/packs/state-guidance-spec.js @@ -0,0 +1,101 @@ +import { expect } from 'chai'; +import { + showOrHideJurisdictionExtras, + showOrHidePuertoRicoExtras, +} from '../../../app/javascript/packs/state-guidance'; + +describe('state-guidance', () => { + describe('showOrHidePuertoRicoExtras', () => { + beforeEach(() => { + document.body.innerHTML = ` +
    +
    + +
    + +
    + `; + }); + + it('includes class display-none if state is not PR', () => { + const forStateCode = 'NY'; + showOrHidePuertoRicoExtras(forStateCode); + const prExtrasElemClassList = document.querySelector('.puerto-rico-extras')?.classList; + + expect(prExtrasElemClassList).to.contain(['puerto-rico-extras', 'display-none']); + }); + + it('does not include class display-none if state is PR', () => { + const forStateCode = 'PR'; + showOrHidePuertoRicoExtras(forStateCode); + const prExtrasElemClassList = document.querySelector('.puerto-rico-extras')?.classList; + + expect(prExtrasElemClassList).to.contain(['puerto-rico-extras']); + }); + }); + + describe('showOrHideJurisdictionExtras', () => { + beforeEach(() => { + document.body.innerHTML = ` +
    +
    + +
    +
    + Default help text + + +
    +
    + `; + }); + + it('includes Texas specific hint text when Texas is selected', () => { + const jurisdictionCode = 'TX'; + showOrHideJurisdictionExtras(jurisdictionCode); + + const allHintTexts = document.querySelectorAll('.jurisdiction-extras [data-state]'); + const texasText = document.querySelectorAll('.jurisdiction-extras [data-state=TX]'); + const nonTexasText = document.querySelectorAll( + '.jurisdiction-extras [data-state].display-none', + ); + + expect(texasText.length).to.eq(1); + expect(texasText[0].classList.contains('display-none')).to.eq(false); + + expect(nonTexasText.length + texasText.length).to.eq(allHintTexts.length); + }); + + it('includes default hint text when no state is selected', () => { + const jurisdictionCode = ''; + showOrHideJurisdictionExtras(jurisdictionCode); + + const allHintTexts = document.querySelectorAll('.jurisdiction-extras [data-state]'); + const defaultText = document.querySelectorAll('.jurisdiction-extras [data-state=default]'); + const nonDefaultText = document.querySelectorAll( + '.jurisdiction-extras [data-state].display-none', + ); + + expect(defaultText.length).to.eq(1); + expect(defaultText[0].classList.contains('display-none')).to.eq(false); + + expect(nonDefaultText.length + defaultText.length).to.eq(allHintTexts.length); + }); + + it('includes default hint text when a state without a state specific hint is selected', () => { + const jurisdictionCode = 'NY'; + showOrHideJurisdictionExtras(jurisdictionCode); + + const allHintTexts = document.querySelectorAll('.jurisdiction-extras [data-state]'); + const defaultText = document.querySelectorAll('.jurisdiction-extras [data-state=default]'); + const nonDefaultText = document.querySelectorAll( + '.jurisdiction-extras [data-state].display-none', + ); + + expect(defaultText.length).to.eq(1); + expect(defaultText[0].classList.contains('display-none')).to.eq(false); + + expect(nonDefaultText.length + defaultText.length).to.eq(allHintTexts.length); + }); + }); +}); diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index bfc0d2b908b..a37d56cf47e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -923,6 +923,14 @@ user.suspend! end + it 'send account disabled push event' do + expect(PushNotification::HttpPush).to receive(:deliver).once. + with(PushNotification::AccountDisabledEvent.new( + user: user, + )) + user.suspend! + end + it 'logs out the suspended user from the active session' do # Add information to session store to allow `exists?` check to work as desired OutOfBandSessionAccessor.new(mock_session_id).put_pii( @@ -985,6 +993,14 @@ user.reinstate! end + it 'send account enabled push event' do + expect(PushNotification::HttpPush).to receive(:deliver).once. + with(PushNotification::AccountEnabledEvent.new( + user: user, + )) + user.reinstate! + end + it 'raises an error if the user is not currently suspended' do user.suspended_at = nil expect(user.analytics).to receive(:user_reinstated).with( diff --git a/spec/presenters/two_factor_authentication/selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/selection_presenter_spec.rb deleted file mode 100644 index 009b7180107..00000000000 --- a/spec/presenters/two_factor_authentication/selection_presenter_spec.rb +++ /dev/null @@ -1,140 +0,0 @@ -require 'rails_helper' - -RSpec.describe TwoFactorAuthentication::SelectionPresenter do - let(:placeholder_presenter_class) do - Class.new(TwoFactorAuthentication::SelectionPresenter) do - def method - :missing - end - end - end - - let(:user) { build(:user) } - - subject(:presenter) { described_class.new(user: user) } - - describe '#render_in' do - it 'renders captured block content' do - view_context = ActionController::Base.new.view_context - - expect(view_context).to receive(:capture) do |*args, &block| - expect(block.call).to eq('content') - end - - presenter.render_in(view_context) { 'content' } - end - end - - describe '#disabled?' do - let(:single_configuration_only) {} - let(:mfa_configuration_count) {} - - before do - allow(presenter).to receive(:single_configuration_only?).and_return(single_configuration_only) - allow(presenter).to receive(:mfa_configuration_count).and_return(mfa_configuration_count) - end - - context 'without single configuration restriction' do - let(:single_configuration_only) { false } - - it 'is an mfa that allows multiple configurations' do - expect(presenter.disabled?).to eq(false) - end - end - - context 'with single configuration only' do - let(:single_configuration_only) { true } - - context 'with default mfa count implementation' do - before do - allow(presenter).to receive(:mfa_configuration_count).and_call_original - end - - it 'is mfa with unimplemented mfa count and single config' do - expect(presenter.disabled?).to eq(false) - end - end - - context 'with no configured mfas' do - let(:mfa_configuration_count) { 0 } - - it 'is configured with no mfa' do - expect(presenter.disabled?).to eq(false) - end - end - - context 'with at least one configured mfa' do - let(:mfa_configuration_count) { 1 } - - it 'is mfa with at least one configured' do - expect(presenter.disabled?).to eq(true) - end - end - end - context 'with configuration' do - let(:single_configuration_only) { true } - let(:mfa_configuration_count) { 1 } - let(:configuration) { create(:phone_configuration, user: user) } - before do - allow(presenter).to receive(:configuration).and_return(configuration) - end - it { expect(presenter.disabled?).to eq(false) } - end - end - - describe '#single_configuration_only?' do - it { expect(presenter.single_configuration_only?).to eq(false) } - end - - describe '#mfa_added_label' do - subject(:mfa_added_label) { presenter.mfa_added_label } - before do - allow(presenter).to receive(:mfa_configuration_count).and_return('1') - end - it 'is a count of configured MFAs' do - expect(presenter.mfa_added_label).to include('added') - end - - context 'with single configuration only' do - before do - allow(presenter).to receive(:single_configuration_only?).and_return(true) - end - - it 'is an empty string' do - expect(presenter.mfa_added_label).to eq('') - end - end - end - - describe '#label' do - context 'with no configuration' do - it 'raises with missing translation' do - expect { placeholder_presenter_class.new(user: user).label }.to raise_error(RuntimeError) - end - end - - context 'with configuration' do - it 'raises with missing translation' do - expect do - placeholder_presenter_class.new(configuration: 1, user: user).label - end.to raise_error(RuntimeError) - end - end - end - - describe '#info' do - context 'with no configuration' do - it 'raises with missing translation' do - expect { placeholder_presenter_class.new(user: user).info }.to raise_error(RuntimeError) - end - end - - context 'with configuration' do - it 'raises with missing translation' do - expect do - placeholder_presenter_class.new(configuration: 1, user: user).info - end.to raise_error(RuntimeError) - end - end - end -end diff --git a/spec/presenters/two_factor_authentication/set_up_auth_app_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/set_up_auth_app_selection_presenter_spec.rb index ada58bceb8d..6118cacb57c 100644 --- a/spec/presenters/two_factor_authentication/set_up_auth_app_selection_presenter_spec.rb +++ b/spec/presenters/two_factor_authentication/set_up_auth_app_selection_presenter_spec.rb @@ -13,7 +13,7 @@ describe '#type' do it 'returns auth_app' do - expect(presenter_without_mfa.type).to eq 'auth_app' + expect(presenter_without_mfa.type).to eq :auth_app end end diff --git a/spec/presenters/two_factor_authentication/phone_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/set_up_phone_selection_presenter_spec.rb similarity index 82% rename from spec/presenters/two_factor_authentication/phone_selection_presenter_spec.rb rename to spec/presenters/two_factor_authentication/set_up_phone_selection_presenter_spec.rb index b22167d42f3..8c9e9137218 100644 --- a/spec/presenters/two_factor_authentication/phone_selection_presenter_spec.rb +++ b/spec/presenters/two_factor_authentication/set_up_phone_selection_presenter_spec.rb @@ -1,15 +1,13 @@ require 'rails_helper' -RSpec.describe TwoFactorAuthentication::PhoneSelectionPresenter do +RSpec.describe TwoFactorAuthentication::SetUpPhoneSelectionPresenter do let(:user_without_mfa) { create(:user) } let(:user_with_mfa) { create(:user, :with_phone) } - let(:presenter_with_mfa) { described_class.new(configuration: phone, user: user_with_mfa) } - let(:presenter_without_mfa) { described_class.new(configuration: phone, user: user_without_mfa) } + let(:presenter_with_mfa) { described_class.new(user: user_with_mfa) } + let(:presenter_without_mfa) { described_class.new(user: user_without_mfa) } describe '#info' do context 'when a user does not have a phone configuration (first time)' do - let(:phone) { nil } - it 'includes a note about choosing voice or sms' do expect(presenter_without_mfa.info). to include(t('two_factor_authentication.two_factor_choice_options.phone_info')) @@ -28,8 +26,6 @@ end describe '#disabled?' do - let(:phone) { build(:phone_configuration, phone: '+1 888 867-5309') } - it { expect(presenter_without_mfa.disabled?).to eq(false) } context 'all phone vendor outage' do @@ -42,7 +38,6 @@ end describe '#mfa_configuration' do - let(:phone) { nil } it 'returns an empty string when user has not configured this authenticator' do expect(presenter_without_mfa.mfa_configuration_description).to eq('') end diff --git a/spec/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter_spec.rb index cb5744107ba..79cc227b621 100644 --- a/spec/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter_spec.rb +++ b/spec/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter_spec.rb @@ -12,7 +12,7 @@ describe '#type' do it 'returns piv_cac' do - expect(presenter_without_mfa.type).to eq 'piv_cac' + expect(presenter_without_mfa.type).to eq :piv_cac end end diff --git a/spec/presenters/two_factor_authentication/set_up_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/set_up_selection_presenter_spec.rb index eec0b2649ba..4b74dbb20fa 100644 --- a/spec/presenters/two_factor_authentication/set_up_selection_presenter_spec.rb +++ b/spec/presenters/two_factor_authentication/set_up_selection_presenter_spec.rb @@ -2,11 +2,7 @@ RSpec.describe TwoFactorAuthentication::SetUpSelectionPresenter do let(:placeholder_presenter_class) do - Class.new(TwoFactorAuthentication::SetUpSelectionPresenter) do - def method - :missing - end - end + Class.new(TwoFactorAuthentication::SetUpSelectionPresenter) end let(:user) { build(:user) } @@ -93,15 +89,21 @@ def method end end + describe '#type' do + it 'raises with missing implementation' do + expect { placeholder_presenter_class.new(user:).type }.to raise_error(NotImplementedError) + end + end + describe '#label' do - it 'raises with missing translation' do - expect { placeholder_presenter_class.new(user: user).label }.to raise_error(RuntimeError) + it 'raises with missing implementation' do + expect { placeholder_presenter_class.new(user:).label }.to raise_error(NotImplementedError) end end describe '#info' do - it 'raises with missing translation' do - expect { placeholder_presenter_class.new(user: user).info }.to raise_error(RuntimeError) + it 'raises with missing implementation' do + expect { placeholder_presenter_class.new(user:).info }.to raise_error(NotImplementedError) end end end diff --git a/spec/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter_spec.rb index 65fd19dcf3f..879e8509f11 100644 --- a/spec/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter_spec.rb +++ b/spec/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter_spec.rb @@ -19,7 +19,7 @@ describe '#type' do it 'returns webauthn_platform' do - expect(presenter_without_mfa.type).to eq 'webauthn_platform' + expect(presenter_without_mfa.type).to eq :webauthn_platform end end diff --git a/spec/presenters/two_factor_authentication/set_up_webauthn_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/set_up_webauthn_selection_presenter_spec.rb index 620868ca79d..1e74d55f580 100644 --- a/spec/presenters/two_factor_authentication/set_up_webauthn_selection_presenter_spec.rb +++ b/spec/presenters/two_factor_authentication/set_up_webauthn_selection_presenter_spec.rb @@ -13,7 +13,7 @@ describe '#type' do it 'returns webauthn' do - expect(presenter_without_mfa.type).to eq 'webauthn' + expect(presenter_without_mfa.type).to eq :webauthn end end diff --git a/spec/presenters/two_factor_authentication/sign_in_auth_app_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/sign_in_auth_app_selection_presenter_spec.rb index 5fe3027a668..857de98bd20 100644 --- a/spec/presenters/two_factor_authentication/sign_in_auth_app_selection_presenter_spec.rb +++ b/spec/presenters/two_factor_authentication/sign_in_auth_app_selection_presenter_spec.rb @@ -10,7 +10,7 @@ describe '#type' do it 'returns auth_app' do - expect(presenter.type).to eq 'auth_app' + expect(presenter.type).to eq :auth_app end end diff --git a/spec/presenters/two_factor_authentication/sign_in_personal_key_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/sign_in_personal_key_selection_presenter_spec.rb index b2993d839e5..98cbdf07633 100644 --- a/spec/presenters/two_factor_authentication/sign_in_personal_key_selection_presenter_spec.rb +++ b/spec/presenters/two_factor_authentication/sign_in_personal_key_selection_presenter_spec.rb @@ -10,7 +10,7 @@ subject(:type) { presenter.type } it 'returns personal key type' do - expect(type).to eq 'personal_key' + expect(type).to eq :personal_key end end diff --git a/spec/presenters/two_factor_authentication/sign_in_phone_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/sign_in_phone_selection_presenter_spec.rb new file mode 100644 index 00000000000..c70580f29fb --- /dev/null +++ b/spec/presenters/two_factor_authentication/sign_in_phone_selection_presenter_spec.rb @@ -0,0 +1,125 @@ +require 'rails_helper' + +RSpec.describe TwoFactorAuthentication::SignInPhoneSelectionPresenter do + let(:user) { create(:user) } + let(:configuration) { create(:phone_configuration, user: user) } + let(:delivery_method) { nil } + + let(:presenter) do + described_class.new(user:, configuration:, delivery_method:) + end + + describe '#type' do + context 'without a defined delivery method' do + let(:delivery_method) { nil } + + it 'returns generic phone type' do + expect(presenter.type).to eq :phone + end + end + + context 'with delivery method' do + let(:delivery_method) { :sms } + + context 'with user having a single configuration' do + it 'returns delivery method' do + expect(presenter.type).to eq :sms + end + end + + context 'with user having multiple configurations' do + let(:user) { create(:user, :with_phone) } + + it 'returns delivery method appended with configuration id' do + expect(presenter.type).to eq "sms_#{configuration.id}".to_sym + end + end + end + end + + describe '#info' do + context 'without a defined delivery method' do + let(:delivery_method) { nil } + + it 'returns the correct translation for setup' do + expect(presenter.info).to eq( + t('two_factor_authentication.two_factor_choice_options.phone_info'), + ) + end + end + + context 'with sms delivery method' do + let(:delivery_method) { :sms } + + it 'returns the correct translation for sms' do + expect(presenter.info).to eq( + t( + 'two_factor_authentication.login_options.sms_info_html', + phone: configuration.masked_phone, + ), + ) + end + end + + context 'with voice delivery method' do + let(:delivery_method) { :voice } + + it 'returns the correct translation for voice' do + expect(presenter.info).to eq( + t( + 'two_factor_authentication.login_options.voice_info_html', + phone: configuration.masked_phone, + ), + ) + end + end + end + + describe '#disabled?' do + let(:phone) { build(:phone_configuration, phone: '+1 888 867-5309') } + + context 'without a defined delivery method' do + let(:delivery_method) { nil } + + it { expect(presenter.disabled?).to eq(false) } + + context 'all phone vendor outage' do + before do + allow_any_instance_of(OutageStatus).to receive(:all_phone_vendor_outage?).and_return(true) + end + + it { expect(presenter.disabled?).to eq(true) } + end + end + + context 'with sms delivery method' do + let(:delivery_method) { :sms } + + it { expect(presenter.disabled?).to eq(false) } + + context 'sms vendor outage' do + before do + allow_any_instance_of(OutageStatus).to receive(:vendor_outage?).with(:sms). + and_return(true) + end + + it { expect(presenter.disabled?).to eq(true) } + end + end + + context 'with voice delivery method' do + let(:delivery_method) { :voice } + + it { expect(presenter.disabled?).to eq(false) } + + context 'voice vendor outage' do + before do + allow_any_instance_of(OutageStatus).to receive(:vendor_outage?).with(:voice). + and_return(true) + end + + it { expect(presenter.disabled?).to eq(true) } + end + end + end +end diff --git a/spec/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.spec.rb b/spec/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.spec.rb index 214bf954038..e2d6d5fa8bb 100644 --- a/spec/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.spec.rb +++ b/spec/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.spec.rb @@ -10,7 +10,7 @@ describe '#type' do it 'returns piv_cac' do - expect(presenter.type).to eq 'piv_cac' + expect(presenter.type).to eq :piv_cac end end diff --git a/spec/presenters/two_factor_authentication/sign_in_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/sign_in_selection_presenter_spec.rb index e31720e78fa..d0ce9e0810c 100644 --- a/spec/presenters/two_factor_authentication/sign_in_selection_presenter_spec.rb +++ b/spec/presenters/two_factor_authentication/sign_in_selection_presenter_spec.rb @@ -2,11 +2,7 @@ RSpec.describe TwoFactorAuthentication::SignInSelectionPresenter do let(:placeholder_presenter_class) do - Class.new(TwoFactorAuthentication::SignInSelectionPresenter) do - def method - :missing - end - end + Class.new(TwoFactorAuthentication::SignInSelectionPresenter) end let(:user) { build(:user) } @@ -27,24 +23,20 @@ def method end describe '#label' do - it 'raises with missing translation' do - expect do - presenter.label - end.to raise_error(RuntimeError) + it 'raises with missing implementation' do + expect { presenter.label }.to raise_error(NotImplementedError) end end describe '#type' do - it 'returns missing as type' do - expect(presenter.type).to eq('missing') + it 'raises with missing implementation' do + expect { presenter.type }.to raise_error(NotImplementedError) end end describe '#info' do - it 'raises with missing translation' do - expect do - presenter.info - end.to raise_error(RuntimeError) + it 'raises with missing implementation' do + expect { presenter.info }.to raise_error(NotImplementedError) end end end diff --git a/spec/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter_spec.rb index 14b7400f57e..2744d974f6b 100644 --- a/spec/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter_spec.rb +++ b/spec/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter_spec.rb @@ -10,7 +10,7 @@ describe '#type' do it 'returns webauthn_platform' do - expect(presenter.type).to eq 'webauthn_platform' + expect(presenter.type).to eq :webauthn_platform end end diff --git a/spec/presenters/two_factor_authentication/sign_in_webauthn_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/sign_in_webauthn_selection_presenter_spec.rb index 4676124561f..7bc097bb33d 100644 --- a/spec/presenters/two_factor_authentication/sign_in_webauthn_selection_presenter_spec.rb +++ b/spec/presenters/two_factor_authentication/sign_in_webauthn_selection_presenter_spec.rb @@ -10,7 +10,7 @@ describe '#type' do it 'returns webauthn' do - expect(presenter.type).to eq 'webauthn' + expect(presenter.type).to eq :webauthn end end diff --git a/spec/presenters/two_factor_authentication/sms_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/sms_selection_presenter_spec.rb deleted file mode 100644 index 54064ce07d3..00000000000 --- a/spec/presenters/two_factor_authentication/sms_selection_presenter_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'rails_helper' - -RSpec.describe TwoFactorAuthentication::SmsSelectionPresenter do - let(:subject) { described_class.new(configuration: phone, user: user) } - let(:user) { build(:user) } - - describe '#type' do - context 'when a user has only one phone configuration' do - let(:user) { create(:user, :with_phone) } - let(:phone) { MfaContext.new(user).phone_configurations.first } - - it 'returns sms' do - expect(subject.type).to eq 'sms' - end - end - - context 'when a user has more than one phone configuration' do - let(:user) { create(:user, :with_phone) } - let(:phone) do - record = create(:phone_configuration, user: user) - user.reload - record - end - - it 'returns sms:id' do - expect(subject.type).to eq "sms_#{phone.id}" - end - end - end - - describe '#info' do - context 'when a user has a phone configuration' do - let(:phone) { build(:phone_configuration, phone: '+1 888 867-5309') } - it 'includes the masked the number' do - expect(subject.info).to include('(***) ***-5309') - end - end - end - - describe '#disabled?' do - let(:phone) { build(:phone_configuration, phone: '+1 888 867-5309') } - it { expect(subject.disabled?).to eq(false) } - - context 'sms vendor outage' do - before do - allow_any_instance_of(OutageStatus).to receive(:vendor_outage?).with(:sms).and_return(true) - end - - it { expect(subject.disabled?).to eq(true) } - end - end -end diff --git a/spec/presenters/two_factor_authentication/voice_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/voice_selection_presenter_spec.rb deleted file mode 100644 index edb1d87d6d3..00000000000 --- a/spec/presenters/two_factor_authentication/voice_selection_presenter_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'rails_helper' - -RSpec.describe TwoFactorAuthentication::VoiceSelectionPresenter do - let(:subject) { described_class.new(configuration: phone, user: user) } - let(:user) { build(:user) } - describe '#type' do - context 'when a user has only one phone configuration' do - let(:user) { create(:user, :with_phone) } - let(:phone) { MfaContext.new(user).phone_configurations.first } - - it 'returns voice' do - expect(subject.type).to eq 'voice' - end - end - - context 'when a user has more than one phone configuration' do - let(:user) { create(:user, :with_phone) } - let(:phone) do - record = create(:phone_configuration, user: user) - user.reload - record - end - - it 'returns voice:id' do - expect(subject.type).to eq "voice_#{phone.id}" - end - end - end - - describe '#info' do - context 'when a user has a phone configuration' do - let(:phone) { build(:phone_configuration, phone: '+1 888 867-5309') } - it 'includes the masked the number' do - expect(subject.info).to include('(***) ***-5309') - end - end - end - - describe '#disabled?' do - let(:phone) { build(:phone_configuration, phone: '+1 888 867-5309') } - it { expect(subject.disabled?).to eq(false) } - - context 'voice vendor outage' do - before do - allow_any_instance_of(OutageStatus).to receive(:vendor_outage?).with(:voice). - and_return(true) - end - - it { expect(subject.disabled?).to eq(true) } - end - end -end diff --git a/spec/presenters/two_factor_login_options_presenter_spec.rb b/spec/presenters/two_factor_login_options_presenter_spec.rb index d1ff9ad4dfc..1386d500dd9 100644 --- a/spec/presenters/two_factor_login_options_presenter_spec.rb +++ b/spec/presenters/two_factor_login_options_presenter_spec.rb @@ -82,8 +82,8 @@ it 'returns classes for mfas associated with account' do expect(options_classes).to eq( [ - TwoFactorAuthentication::SmsSelectionPresenter, - TwoFactorAuthentication::VoiceSelectionPresenter, + TwoFactorAuthentication::SignInPhoneSelectionPresenter, + TwoFactorAuthentication::SignInPhoneSelectionPresenter, TwoFactorAuthentication::SignInWebauthnSelectionPresenter, TwoFactorAuthentication::SignInBackupCodeSelectionPresenter, TwoFactorAuthentication::SignInPivCacSelectionPresenter, @@ -114,8 +114,8 @@ it 'returns all mfas associated with account' do expect(options_classes).to eq( [ - TwoFactorAuthentication::SmsSelectionPresenter, - TwoFactorAuthentication::VoiceSelectionPresenter, + TwoFactorAuthentication::SignInPhoneSelectionPresenter, + TwoFactorAuthentication::SignInPhoneSelectionPresenter, TwoFactorAuthentication::SignInWebauthnSelectionPresenter, TwoFactorAuthentication::SignInBackupCodeSelectionPresenter, TwoFactorAuthentication::SignInPivCacSelectionPresenter, @@ -145,8 +145,8 @@ it 'returns all mfas associated with account' do expect(options_classes).to eq( [ - TwoFactorAuthentication::SmsSelectionPresenter, - TwoFactorAuthentication::VoiceSelectionPresenter, + TwoFactorAuthentication::SignInPhoneSelectionPresenter, + TwoFactorAuthentication::SignInPhoneSelectionPresenter, TwoFactorAuthentication::SignInWebauthnSelectionPresenter, TwoFactorAuthentication::SignInBackupCodeSelectionPresenter, TwoFactorAuthentication::SignInPivCacSelectionPresenter, diff --git a/spec/presenters/two_factor_options_presenter_spec.rb b/spec/presenters/two_factor_options_presenter_spec.rb index d644a20b6e4..97233969597 100644 --- a/spec/presenters/two_factor_options_presenter_spec.rb +++ b/spec/presenters/two_factor_options_presenter_spec.rb @@ -27,7 +27,7 @@ expect(presenter.options.map(&:class)).to eq [ TwoFactorAuthentication::SetUpWebauthnPlatformSelectionPresenter, TwoFactorAuthentication::SetUpAuthAppSelectionPresenter, - TwoFactorAuthentication::PhoneSelectionPresenter, + TwoFactorAuthentication::SetUpPhoneSelectionPresenter, TwoFactorAuthentication::SetUpBackupCodeSelectionPresenter, TwoFactorAuthentication::SetUpWebauthnSelectionPresenter, TwoFactorAuthentication::SetUpPivCacSelectionPresenter, @@ -76,7 +76,7 @@ expect(presenter.options.map(&:class)).to eq [ TwoFactorAuthentication::SetUpWebauthnPlatformSelectionPresenter, TwoFactorAuthentication::SetUpAuthAppSelectionPresenter, - TwoFactorAuthentication::PhoneSelectionPresenter, + TwoFactorAuthentication::SetUpPhoneSelectionPresenter, TwoFactorAuthentication::SetUpBackupCodeSelectionPresenter, TwoFactorAuthentication::SetUpWebauthnSelectionPresenter, TwoFactorAuthentication::SetUpPivCacSelectionPresenter, diff --git a/spec/requests/rack_attack_spec.rb b/spec/requests/rack_attack_spec.rb index 982c90ec627..075633b2af2 100644 --- a/spec/requests/rack_attack_spec.rb +++ b/spec/requests/rack_attack_spec.rb @@ -163,6 +163,37 @@ expect(analytics). to have_received(:track_event).with('Rate Limit Triggered', type: 'req/ip') end + + it 'logs the service provider' do + analytics = FakeAnalytics.new + allow(Analytics).to receive(:new).and_return(analytics) + + client_id = 'urn:gov:gsa:openidconnect:sp:server' + state = SecureRandom.hex + nonce = SecureRandom.hex + params = { + client_id: client_id, + response_type: 'code', + acr_values: Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, + scope: 'openid email profile:name social_security_number', + redirect_uri: 'http://localhost:7654/auth/result', + state: state, + nonce: nonce, + prompt: 'select_account', + } + + get( + openid_connect_authorize_path, + params: params, + headers: { REMOTE_ADDR: '1.2.3.4' }, + ) + requests_per_ip_limit.times do + get '/', headers: { REMOTE_ADDR: '1.2.3.4' } + end + + expect(Analytics).to have_received(:new).with(include(sp: client_id)).at_least(:once) + expect(analytics).to have_logged_event('Rate Limit Triggered', type: 'req/ip') + end end end diff --git a/spec/services/account_reset/delete_account_spec.rb b/spec/services/account_reset/delete_account_spec.rb index fe561d2dbc5..582236929f7 100644 --- a/spec/services/account_reset/delete_account_spec.rb +++ b/spec/services/account_reset/delete_account_spec.rb @@ -3,10 +3,6 @@ RSpec.describe AccountReset::DeleteAccount do include AccountResetHelper - let(:expired_token_message) do - t('errors.account_reset.granted_token_expired', app_name: APP_NAME) - end - let(:expired_token_error) { { token: [expired_token_message] } } let(:user) { create(:user) } let(:request) { FakeRequest.new } let(:analytics) { FakeAnalytics.new } @@ -57,7 +53,6 @@ it 'logs attempts api event with success true if the token is good' do expect(fake_attempts_tracker).to receive(:account_reset_account_deleted).with( success: true, - failure_reason: nil, ) create_account_reset_request_for(user, service_provider.issuer) @@ -69,7 +64,6 @@ it 'logs attempts api event with failure reason if the token is expired' do expect(fake_attempts_tracker).to receive(:account_reset_account_deleted).with( success: false, - failure_reason: expired_token_error, ) create_account_reset_request_for(user, service_provider.issuer) diff --git a/spec/services/account_reset/validate_granted_token_spec.rb b/spec/services/account_reset/validate_granted_token_spec.rb index cde471c1c7b..de23fbaf3d3 100644 --- a/spec/services/account_reset/validate_granted_token_spec.rb +++ b/spec/services/account_reset/validate_granted_token_spec.rb @@ -3,10 +3,6 @@ RSpec.describe AccountReset::ValidateGrantedToken do include AccountResetHelper - let(:expired_token_message) do - t('errors.account_reset.granted_token_expired', app_name: APP_NAME) - end - let(:expired_token_error) { { token: [expired_token_message] } } let(:user) { create(:user) } let(:request) { FakeRequest.new } let(:analytics) { FakeAnalytics.new } @@ -33,7 +29,6 @@ it 'logs attempts api event with failure reason if the token is expired' do expect(fake_attempts_tracker).to receive(:account_reset_account_deleted).with( success: false, - failure_reason: expired_token_error, ) create_account_reset_request_for(user, service_provider.issuer) diff --git a/spec/services/active_profile_encryptor_spec.rb b/spec/services/active_profile_encryptor_spec.rb deleted file mode 100644 index 568ff20e0de..00000000000 --- a/spec/services/active_profile_encryptor_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'rails_helper' - -RSpec.describe ActiveProfileEncryptor do - describe '#call' do - it 'encrypts the profile' do - decrypted_pii = { ssn: '1234' }.to_json - user_session = { decrypted_pii: decrypted_pii } - profile = create(:profile, :active, :verified, pii: { ssn: '1234' }) - user = profile.user - password = user.password - current_pii = Pii::Attributes.new_from_json(decrypted_pii) - - allow(user).to receive(:active_profile).and_return(profile) - allow(profile).to receive(:encrypt_pii) - allow(profile).to receive(:save!) - allow(Pii::Attributes).to receive(:new_from_json).with(decrypted_pii). - and_return(current_pii) - - ActiveProfileEncryptor.new(user, user_session, password).call - - expect(profile).to have_received(:encrypt_pii).with(current_pii, password) - expect(profile).to have_received(:save!) - end - end -end diff --git a/spec/services/doc_auth/acuant/issuer_types_spec.rb b/spec/services/doc_auth/acuant/issuer_types_spec.rb new file mode 100644 index 00000000000..163c90946e1 --- /dev/null +++ b/spec/services/doc_auth/acuant/issuer_types_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +RSpec.describe DocAuth::Acuant::IssuerTypes do + describe '.from_int' do + it 'is a result code for the int' do + issuer_type = DocAuth::Acuant::IssuerTypes.from_int(1) + expect(issuer_type).to be_a(DocAuth::Acuant::IssuerTypes::IssuerType) + end + + it 'is nil when there is no matching code' do + issuer_type = DocAuth::Acuant::IssuerTypes.from_int(999) + expect(issuer_type).to be_nil + end + end +end diff --git a/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb b/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb index 5795ba59d60..178b6027ffe 100644 --- a/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb +++ b/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb @@ -39,23 +39,25 @@ alert_failure_count: 2, tamper_result: 'Passed', classification_info: { - 'Back' => { - 'ClassName' => 'Identification Card', - 'Issue' => '2014', - 'IssueType' => 'Back', - 'Name' => 'North Dakota (ND) Back', - 'IssuerCode' => 'ND', - 'IssuerName' => 'North Dakota', - 'CountryCode' => 'USA', + Back: { + ClassName: 'Identification Card', + Issue: '2014', + IssueType: 'Back', + Name: 'North Dakota (ND) Back', + IssuerCode: 'ND', + IssuerName: 'North Dakota', + CountryCode: 'USA', + IssuerType: 'StateProvince', }, - 'Front' => { - 'ClassName' => 'Identification Card', - 'Issue' => '2014', - 'IssueType' => 'Non-Driver Identification Card', - 'Name' => 'North Dakota (ND) Non-Driver Identification Card', - 'IssuerCode' => 'ND', - 'IssuerName' => 'North Dakota', - 'CountryCode' => 'USA', + Front: { + ClassName: 'Identification Card', + Issue: '2014', + IssueType: 'Non-Driver Identification Card', + Name: 'North Dakota (ND) Non-Driver Identification Card', + IssuerCode: 'ND', + IssuerName: 'North Dakota', + CountryCode: 'USA', + IssuerType: 'StateProvince', }, }, address_line2_present: true, @@ -393,23 +395,25 @@ alert_failure_count: 2, tamper_result: 'Passed', classification_info: { - 'Back' => { - 'ClassName' => 'Tribal Identification', - 'Issue' => '2014', - 'IssueType' => 'Back', - 'Name' => 'Cowlitz Indian Tribe Back', - 'IssuerCode' => 'ND', - 'IssuerName' => 'Cowlitz Indian Tribe', - 'CountryCode' => 'USA', + Back: { + ClassName: 'Tribal Identification', + Issue: '2014', + IssueType: 'Back', + Name: 'Cowlitz Indian Tribe Back', + IssuerCode: 'ND', + IssuerName: 'Cowlitz Indian Tribe', + CountryCode: 'USA', + IssuerType: 'StateProvince', }, - 'Front' => { - 'ClassName' => 'Tribal Identification', - 'Issue' => '2014', - 'IssueType' => 'Tribal Identification Card', - 'Name' => 'Cowlitz Indian Tribe Tribal Identification', - 'IssuerCode' => 'ND', - 'IssuerName' => 'Cowlitz Indian Tribe', - 'CountryCode' => 'USA', + Front: { + ClassName: 'Tribal Identification', + Issue: '2014', + IssueType: 'Tribal Identification Card', + Name: 'Cowlitz Indian Tribe Tribal Identification', + IssuerCode: 'ND', + IssuerName: 'Cowlitz Indian Tribe', + CountryCode: 'USA', + IssuerType: 'StateProvince', }, }, address_line2_present: true, @@ -472,23 +476,25 @@ alert_failure_count: 2, tamper_result: 'Passed', classification_info: { - 'Back' => { - 'ClassName' => 'Identification Card', - 'Issue' => '2014', - 'IssueType' => 'Back', - 'Name' => 'North Dakota (ND) Back', - 'IssuerCode' => 'ND', - 'IssuerName' => 'North Dakota', - 'CountryCode' => 'CAN', + Back: { + ClassName: 'Identification Card', + Issue: '2014', + IssueType: 'Back', + Name: 'North Dakota (ND) Back', + IssuerCode: 'ND', + IssuerName: 'North Dakota', + CountryCode: 'CAN', + IssuerType: 'StateProvince', }, - 'Front' => { - 'ClassName' => 'Identification Card', - 'Issue' => '2014', - 'IssueType' => 'Non-Driver Identification Card', - 'Name' => 'North Dakota (ND) Non-Driver Identification Card', - 'IssuerCode' => 'ND', - 'IssuerName' => 'North Dakota', - 'CountryCode' => 'CAN', + Front: { + ClassName: 'Identification Card', + Issue: '2014', + IssueType: 'Non-Driver Identification Card', + Name: 'North Dakota (ND) Non-Driver Identification Card', + IssuerCode: 'ND', + IssuerName: 'North Dakota', + CountryCode: 'CAN', + IssuerType: 'StateProvince', }, }, address_line2_present: true, diff --git a/spec/services/doc_auth/classification_concern_spec.rb b/spec/services/doc_auth/classification_concern_spec.rb new file mode 100644 index 00000000000..116192e1919 --- /dev/null +++ b/spec/services/doc_auth/classification_concern_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +RSpec.describe DocAuth::ClassificationConcern do + let(:class_name) { 'Identification Card' } + let(:country_code) { 'USA' } + let(:issuer_type) { 'StateProvince' } + let(:info) do + { + Front: { + ClassName: class_name, + CountryCode: country_code, + IssuerType: issuer_type, + }, + Back: { + ClassName: class_name, + CountryCode: country_code, + IssuerType: issuer_type, + }, + } + end + + subject do + Class.new do + include DocAuth::ClassificationConcern + attr_reader :classification_info + def initialize(classification_info) + @classification_info = classification_info + end + end.new(info) + end + + describe '#id_type_supported?' do + context 'with state issued identification card' do + it 'returns true' do + expect(subject.id_type_supported?).to eq(true) + end + end + + context 'with US passport card' do + let(:issuer_type) { 'Country' } + it 'returns false' do + expect(subject.id_type_supported?).to eq(false) + end + end + + context 'with state issued drivers license' do + let(:class_name) { 'Drivers License' } + it 'returns true' do + expect(subject.id_type_supported?).to eq(true) + end + end + end +end diff --git a/spec/services/doc_auth/error_generator_spec.rb b/spec/services/doc_auth/error_generator_spec.rb index 5cb19447916..2d78483712f 100644 --- a/spec/services/doc_auth/error_generator_spec.rb +++ b/spec/services/doc_auth/error_generator_spec.rb @@ -15,7 +15,8 @@ IssueType: 'ePassport', Name: 'United States (USA) ePassport', IssuerCode: 'USA', - IssuerName: 'United States' } + IssuerName: 'United States', + IssuerType: 'Country' } end let(:unknown_classification_details) do { ClassName: 'Unknown', @@ -25,6 +26,15 @@ IssuerCode: nil, IssuerName: nil } end + let(:vhic_classification_details) do + { ClassName: 'Identification Card', + Issue: '2020', + IssueType: 'Veteran Health Identification Card', + Name: 'United States (USA) Veteran Health Identification Card', + IssuerCode: 'USA', + IssuerName: 'United States', + IssuerType: 'Country' } + end def build_error_info( doc_result: nil, @@ -262,6 +272,28 @@ def build_error_info( expect(output[:front]).to contain_exactly(DocAuth::Errors::CARD_TYPE) expect(output[:hints]).to eq(true) end + + it 'DocAuthResult is success with VHIC' do + error_info = build_error_info( + doc_result: 'Passed', + passed: [{ name: 'Not a known alert', result: 'Passed' }], + failed: [], + classification_info: { Back: vhic_classification_details, + Front: vhic_classification_details }, + ) + + expect(warn_notifier).to receive(:call). + with(hash_including(:response_info, :message, :unknown_alerts)).once + + output = described_class.new(config).generate_doc_auth_errors(error_info) + + expect(output.keys).to contain_exactly(:general, :front, :back, :hints) + expect(output[:general]).to contain_exactly(DocAuth::Errors::DOC_TYPE_CHECK) + expect(output[:front]).to contain_exactly(DocAuth::Errors::CARD_TYPE) + expect(output[:back]).to contain_exactly(DocAuth::Errors::CARD_TYPE) + expect(output[:hints]).to eq(true) + end + it 'DocAuthResult is failed with unknown doc type' do error_info = build_error_info( doc_result: 'Failed', @@ -467,4 +499,41 @@ def build_error_info( expect(output[:hints]).to eq(false) end end + + context 'with both doc type error and image metric error' do + let(:metrics) do + { + front: { + 'HorizontalResolution' => 300, + 'VerticalResolution' => 300, + 'SharpnessMetric' => 50, + 'GlareMetric' => 50, + }, + back: { + 'HorizontalResolution' => 300, + 'VerticalResolution' => 300, + 'SharpnessMetric' => 50, + 'GlareMetric' => 50, + }, + } + end + it 'generate doc type error' do + metrics[:front]['HorizontalResolution'] = 50 + error_info = build_error_info( + doc_result: 'Failed', + failed: [{ name: '2D Barcode Read', result: 'Attention' }], + classification_info: { Back: vhic_classification_details, + Front: vhic_classification_details }, + image_metrics: metrics, + ) + + output = described_class.new(config).generate_doc_auth_errors(error_info) + expect(output.keys).to contain_exactly(:general, :front, :back, :hints) + + expect(output[:general]).to contain_exactly(DocAuth::Errors::DOC_TYPE_CHECK) + expect(output[:back]).to contain_exactly(DocAuth::Errors::CARD_TYPE) + expect(output[:front]).to contain_exactly(DocAuth::Errors::CARD_TYPE) + expect(output[:hints]).to eq(true) + end + end end diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb index caa2df20f6f..c95940c1a0e 100644 --- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb @@ -133,8 +133,8 @@ 'DocAuthTamperResult' => 'Passed', 'DocAuthTamperSensitivity' => 'Normal', classification_info: { - Front: a_hash_including(:ClassName, :CountryCode), - Back: a_hash_including(:ClassName, :CountryCode), + Front: a_hash_including(:ClassName, :CountryCode, :IssuerType), + Back: a_hash_including(:ClassName, :CountryCode, :IssuerType), }, ) passed_alerts = response_hash.dig(:processed_alerts, :passed) @@ -160,6 +160,24 @@ it 'mark doc type as supported' do expect(response.doc_type_supported?).to eq(true) end + + context 'when identification card issued by a country' do + let(:success_response) do + body = JSON.parse(LexisNexisFixtures.true_id_response_success_3).tap do |json| + doc_class_node = json['Products'].first['ParameterDetails']. + select { |f| f['Name'] == 'DocClassName' && f['Group'] == 'AUTHENTICATION_RESULT' } + doc_class_node.first['Values'].first['Value'] = 'Identification Card' + doc_issuer_type = json['Products'].first['ParameterDetails']. + select { |f| f['Name'] == 'DocIssuerType' && f['Group'] == 'AUTHENTICATION_RESULT' } + doc_issuer_type.first['Values'].first['Value'] = 'Country' + end.to_json + instance_double(Faraday::Response, status: 200, body: body) + end + it 'mark doc type as not supported' do + expect(response.doc_type_supported?).to eq(false) + expect(response.success?).to eq(false) + end + end end context 'when there is no address line 2' do @@ -338,6 +356,7 @@ def get_decision_product(resp) 'DocClassName' => 'Drivers License', 'DocumentName' => 'Connecticut (CT) Driver License', 'DocIssuerCode' => 'CT', + 'DocIssuerType' => 'StateProvince', 'DocIssuerName' => 'Connecticut', 'DocIssue' => '2009', 'DocIssueType' => 'Driver License', @@ -345,8 +364,8 @@ def get_decision_product(resp) 'OrientationChanged' => 'false', 'PresentationChanged' => 'false', classification_info: { - Front: a_hash_including(:ClassName, :CountryCode), - Back: a_hash_including(:ClassName, :CountryCode), + Front: a_hash_including(:ClassName, :CountryCode, :IssuerType), + Back: a_hash_including(:ClassName, :CountryCode, :IssuerType), }, ) end @@ -554,9 +573,26 @@ def get_decision_product(resp) context 'when country code is not supported' do let(:success_response) do body = JSON.parse(LexisNexisFixtures.true_id_response_success_3).tap do |json| - doc_class_node = json['Products'].first['ParameterDetails']. + doc_country_node = json['Products'].first['ParameterDetails']. select { |f| f['Name'] == 'Fields_CountryCode' && f['Group'] == 'IDAUTH_FIELD_DATA' } - doc_class_node.first['Values'].first['Value'] = 'CAN' + doc_country_node.first['Values'].first['Value'] = 'CAN' + end.to_json + instance_double(Faraday::Response, status: 200, body: body) + end + it 'identify as unsupported doc type' do + is_expected.to eq(false) + end + end + + context 'when id is federal identification card' do + let(:success_response) do + body = JSON.parse(LexisNexisFixtures.true_id_response_success_3).tap do |json| + doc_class_node = json['Products'].first['ParameterDetails']. + select { |f| f['Name'] == 'DocClassName' && f['Group'] == 'AUTHENTICATION_RESULT' } + doc_class_node.first['Values'].first['Value'] = 'Identification Card' + doc_issuer_type = json['Products'].first['ParameterDetails']. + select { |f| f['Name'] == 'DocIssuerType' && f['Group'] == 'AUTHENTICATION_RESULT' } + doc_issuer_type.first['Values'].first['Value'] = 'Country' end.to_json instance_double(Faraday::Response, status: 200, body: body) end @@ -564,5 +600,30 @@ def get_decision_product(resp) is_expected.to eq(false) end end + + context 'when id is federal ID and image dpi is low' do + let(:error_response) do + body = JSON.parse(LexisNexisFixtures.true_id_response_success_3).tap do |json| + doc_class_node = json['Products'].first['ParameterDetails']. + select { |f| f['Name'] == 'DocClassName' && f['Group'] == 'AUTHENTICATION_RESULT' } + doc_class_node.first['Values'].first['Value'] = 'Identification Card' + doc_issuer_type = json['Products'].first['ParameterDetails']. + select { |f| f['Name'] == 'DocIssuerType' && f['Group'] == 'AUTHENTICATION_RESULT' } + doc_issuer_type.first['Values'].first['Value'] = 'Country' + + image_metric_resolution = json['Products'].first['ParameterDetails']. + select do |f| + f['Group'] == 'IMAGE_METRICS_RESULT' && + f['Name'] == 'HorizontalResolution' + end + image_metric_resolution.first['Values'].first['Value'] = 50 + end.to_json + instance_double(Faraday::Response, status: 200, body: body) + end + it 'mark doc type as not supported' do + response = described_class.new(error_response, config) + expect(response.doc_type_supported?).to eq(false) + end + end end end diff --git a/spec/services/doc_auth/mock/result_response_spec.rb b/spec/services/doc_auth/mock/result_response_spec.rb index ce74c806ef5..09a81706549 100644 --- a/spec/services/doc_auth/mock/result_response_spec.rb +++ b/spec/services/doc_auth/mock/result_response_spec.rb @@ -514,8 +514,53 @@ end it 'returns doc type as not supported' do expect(response.doc_type_supported?).to eq(false) + expect(response.errors).to eq( + general: [DocAuth::Errors::DOC_TYPE_CHECK], + front: [DocAuth::Errors::CARD_TYPE], + back: [DocAuth::Errors::CARD_TYPE], + hints: true, + ) + end + end + + context 'with a passed yaml file containing unsupported doc type and bad image metrics' do + let(:input) do + <<~YAML + doc_auth_result: Passed + classification_info: + Front: + ClassName: Identification Card + CountryCode: USA + IssuerType: Country + Back: + ClassName: Identification Card + CountryCode: USA + IssuerType: StateProvince + image_metrics: + front: + HorizontalResolution: 50 + VerticalResolution: 300 + SharpnessMetric: 50 + GlareMetric: 50 + back: + HorizontalResolution: 300 + VerticalResolution: 300, + SharpnessMetric: 50, + GlareMetric: 50 + YAML + end + it 'returns doc type as not supported and generate errors for doc type' do + expect(response.doc_type_supported?).to eq(false) + expect(response.errors).to eq( + general: [DocAuth::Errors::DOC_TYPE_CHECK], + front: [DocAuth::Errors::CARD_TYPE], + hints: true, + ) + expect(response.exception).to be_nil + expect(response.success?).to eq(false) end end + context 'with a yaml file that does not include classification info' do let(:input) do <<~YAML diff --git a/spec/services/form_response_spec.rb b/spec/services/form_response_spec.rb index 9898d2e5eb7..76f3659702d 100644 --- a/spec/services/form_response_spec.rb +++ b/spec/services/form_response_spec.rb @@ -78,7 +78,9 @@ response2 = FormResponse.new(success: false, errors: errors2) combined_response = response1.merge(response2) - expect(combined_response.to_h[:error_details]).to eq(email_language: [:blank, :invalid]) + expect(combined_response.to_h[:error_details]).to eq( + email_language: { blank: true, invalid: true }, + ) end it 'merges hash and ActiveModel::Errors' do @@ -93,7 +95,7 @@ expect(combined_response.errors).to eq( email_language: ['Language cannot be blank', 'Language is not valid'], ) - expect(combined_response.to_h[:error_details]).to eq(email_language: [:blank]) + expect(combined_response.to_h[:error_details]).to eq(email_language: { blank: true }) end it 'returns true if one is false and one is true' do @@ -146,13 +148,32 @@ email_language: ['Language cannot be blank'], }, error_details: { - email_language: [:blank], + email_language: { blank: true }, }, } expect(response.to_h).to eq response_hash end + context 'without error type' do + it 'falls back to message as key for details' do + errors = ActiveModel::Errors.new(build_stubbed(:user)) + errors.add(:email_language, :blank) + response = FormResponse.new(success: false, errors: errors) + response_hash = { + success: false, + errors: { + email_language: [t('errors.messages.blank')], + }, + error_details: { + email_language: { blank: true }, + }, + } + + expect(response.to_h).to eq response_hash + end + end + it 'omits details if errors are empty' do errors = ActiveModel::Errors.new(build_stubbed(:user)) response = FormResponse.new(success: true, errors: errors) @@ -177,6 +198,25 @@ expect(combined_response.to_h).to eq response_hash end + context 'with error detail symbol defined as type option on error' do + it 'returns a hash with success, errors, and error_details keys' do + errors = ActiveModel::Errors.new(build_stubbed(:user)) + errors.add(:email_language, 'Language cannot be blank', type: :blank) + response = FormResponse.new(success: false, errors: errors) + response_hash = { + success: false, + errors: { + email_language: ['Language cannot be blank'], + }, + error_details: { + email_language: { blank: true }, + }, + } + + expect(response.to_h).to eq response_hash + end + end + context 'with serialize_error_details_only' do it 'excludes errors from the hash' do errors = ActiveModel::Errors.new(build_stubbed(:user)) @@ -190,7 +230,7 @@ expect(response.to_h).to eq( success: false, error_details: { - email_language: [:blank], + email_language: { blank: true }, }, ) end diff --git a/spec/services/push_notification/account_disabled_event_spec.rb b/spec/services/push_notification/account_disabled_event_spec.rb new file mode 100644 index 00000000000..99ceec32c3d --- /dev/null +++ b/spec/services/push_notification/account_disabled_event_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +RSpec.describe PushNotification::AccountDisabledEvent do + include Rails.application.routes.url_helpers + + subject(:event) do + PushNotification::AccountDisabledEvent.new(user: user) + end + + let(:user) { build(:user) } + + describe '#event_type' do + it 'is the RISC event type' do + expect(event.event_type).to eq(PushNotification::AccountDisabledEvent::EVENT_TYPE) + end + end + + describe '#payload' do + let(:iss_sub) { SecureRandom.uuid } + + subject(:payload) { event.payload(iss_sub: iss_sub) } + + it 'is a subject with the provided iss_sub and reason' do + expect(payload).to eq( + subject: { + subject_type: 'iss-sub', + sub: iss_sub, + iss: root_url, + }, + reason: 'account-suspension', + ) + end + end +end diff --git a/spec/services/push_notification/account_enabled_event_spec.rb b/spec/services/push_notification/account_enabled_event_spec.rb new file mode 100644 index 00000000000..17be59943c1 --- /dev/null +++ b/spec/services/push_notification/account_enabled_event_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +RSpec.describe PushNotification::AccountEnabledEvent do + include Rails.application.routes.url_helpers + + subject(:event) do + PushNotification::AccountEnabledEvent.new(user: user) + end + + let(:user) { build(:user) } + + describe '#event_type' do + it 'is the RISC event type' do + expect(event.event_type).to eq(PushNotification::AccountEnabledEvent::EVENT_TYPE) + end + end + + describe '#payload' do + let(:iss_sub) { SecureRandom.uuid } + + subject(:payload) { event.payload(iss_sub: iss_sub) } + + it 'is a subject with the provided iss_sub ' do + expect(payload).to eq( + subject: { + subject_type: 'iss-sub', + sub: iss_sub, + iss: root_url, + }, + ) + end + end +end diff --git a/spec/services/user_profiles_encryptor_spec.rb b/spec/services/user_profiles_encryptor_spec.rb new file mode 100644 index 00000000000..4602d8fb5d6 --- /dev/null +++ b/spec/services/user_profiles_encryptor_spec.rb @@ -0,0 +1,121 @@ +require 'rails_helper' + +RSpec.describe UserProfilesEncryptor do + describe '#call' do + let(:user_session) { {}.with_indifferent_access } + let(:pii) { Pii::Attributes.new(ssn: '1234') } + let(:profile) { create(:profile, :active, :verified, pii: pii.to_h) } + let(:user) { profile.user } + let(:password) { 'a new and incredibly exciting password' } + + before do + Pii::Cacher.new(user, user_session).save_decrypted_pii(pii, profile.id) + end + + context 'when the user has an active profile' do + it 'encrypts the PII for the active profile with the password' do + encryptor = UserProfilesEncryptor.new( + user: user, + user_session: user_session, + password: password, + ) + encryptor.encrypt + + profile.reload + + personal_key = PersonalKeyGenerator.new(user).normalize(encryptor.personal_key) + + decrypted_profile_pii = profile.decrypt_pii(password) + decrypted_profile_recovery_pii = profile.recover_pii(personal_key) + + expect(pii).to eq(decrypted_profile_pii) + expect(pii).to eq(decrypted_profile_recovery_pii) + expect(user.valid_personal_key?(personal_key)).to eq(true) + end + end + + context 'when the user has a pending profile' do + let(:profile) { create(:profile, :verify_by_mail_pending, :verified, pii: pii.to_h) } + + it 'encrypts the PII for the pending profile with the password' do + encryptor = UserProfilesEncryptor.new( + user: user, + user_session: user_session, + password: password, + ) + encryptor.encrypt + + profile.reload + + personal_key = PersonalKeyGenerator.new(user).normalize(encryptor.personal_key) + + decrypted_profile_pii = profile.decrypt_pii(password) + decrypted_profile_recovery_pii = profile.recover_pii(personal_key) + + expect(pii).to eq(decrypted_profile_pii) + expect(pii).to eq(decrypted_profile_recovery_pii) + expect(user.valid_personal_key?(personal_key)).to eq(true) + end + end + + context 'when the user has an active and a pending profile' do + let(:active_pii) { pii } + let(:active_profile) { profile } + let(:pending_pii) { Pii::Attributes.new(ssn: '5555') } + let(:pending_profile) do + create( + :profile, + :verify_by_mail_pending, + :verified, + pii: pending_pii.to_h, + user: user, + ) + end + + before do + Pii::Cacher.new(user, user_session).save_decrypted_pii(pending_pii, pending_profile.id) + end + + it 'encrypts the PII for both profiles with the password' do + encryptor = UserProfilesEncryptor.new( + user: user, + user_session: user_session, + password: password, + ) + encryptor.encrypt + + active_profile.reload + pending_profile.reload + + decrypted_active_profile_pii = active_profile.decrypt_pii(password) + decrypted_pending_profile_pii = pending_profile.decrypt_pii(password) + + expect(decrypted_active_profile_pii).to eq(active_pii) + expect(decrypted_pending_profile_pii).to eq(pending_pii) + end + + it 'sets the pending profile personal key as the personal key' do + encryptor = UserProfilesEncryptor.new( + user: user, + user_session: user_session, + password: password, + ) + encryptor.encrypt + + active_profile.reload + pending_profile.reload + + personal_key = PersonalKeyGenerator.new(user).normalize(encryptor.personal_key) + + expect do + active_profile.recover_pii(personal_key) + end.to raise_error(Encryption::EncryptionError) + + decrypted_pending_profile_recovery_pii = pending_profile.recover_pii(personal_key) + expect(decrypted_pending_profile_recovery_pii).to eq(pending_pii) + + expect(user.valid_personal_key?(personal_key)).to eq(true) + end + end + end +end diff --git a/spec/support/doc_pii_helper.rb b/spec/support/doc_pii_helper.rb index a1d20041ab3..2f9be8fb391 100644 --- a/spec/support/doc_pii_helper.rb +++ b/spec/support/doc_pii_helper.rb @@ -2,14 +2,27 @@ module DocPiiHelper def pii_like_keypaths [ [:pii], - [:name, :dob, :dob_min_age, :address1, :state, :zipcode, :jurisdiction], - [:errors, :name], [:error_details, :name], - [:errors, :dob], [:error_details, :dob], - [:errors, :dob_min_age], [:error_details, :dob_min_age], - [:errors, :address1], [:error_details, :address1], - [:errors, :state], [:error_details, :state], - [:errors, :zipcode], [:error_details, :zipcode], - [:errors, :jurisdiction], [:error_details, :jurisdiction] + [:errors, :name], + [:error_details, :name], + [:error_details, :name, :name], + [:errors, :dob], + [:error_details, :dob], + [:error_details, :dob, :dob], + [:errors, :dob_min_age], + [:error_details, :dob_min_age], + [:error_details, :dob_min_age, :dob_min_age], + [:errors, :address1], + [:error_details, :address1], + [:error_details, :address1, :address1], + [:errors, :state], + [:error_details, :state], + [:error_details, :state, :state], + [:errors, :zipcode], + [:error_details, :zipcode], + [:error_details, :zipcode, :zipcode], + [:errors, :jurisdiction], + [:error_details, :jurisdiction], + [:error_details, :jurisdiction, :jurisdiction], ] end end diff --git a/spec/support/fake_analytics.rb b/spec/support/fake_analytics.rb index 07dddee9acc..bf1ce740c10 100644 --- a/spec/support/fake_analytics.rb +++ b/spec/support/fake_analytics.rb @@ -56,7 +56,7 @@ def track_event(event, original_attributes = {}) ERROR end - check_recursive.call(val, [key]) + check_recursive.call(val, current_keypath) end when Array value.each { |val| check_recursive.call(val, keypath) } diff --git a/spec/support/fake_attempts_tracker.rb b/spec/support/fake_attempts_tracker.rb index fa0add2a3cc..768a9c045ff 100644 --- a/spec/support/fake_attempts_tracker.rb +++ b/spec/support/fake_attempts_tracker.rb @@ -13,17 +13,5 @@ def track_event(event, attributes = {}) events[event] << attributes nil end - - def parse_failure_reason(result) - return result.to_h[:error_details] || result.errors.presence - end - - def track_mfa_submit_event(_attributes) - # no-op - end - - def browser_attributes - {} - end end end diff --git a/spec/support/idv_examples/sp_requested_attributes.rb b/spec/support/idv_examples/sp_requested_attributes.rb index 53e942c1d2f..d2ee8560579 100644 --- a/spec/support/idv_examples/sp_requested_attributes.rb +++ b/spec/support/idv_examples/sp_requested_attributes.rb @@ -22,22 +22,20 @@ expect(current_path).to eq(sign_up_completed_path) - within('.requested-attributes') do - expect(page).to have_content t('help_text.requested_attributes.email') - expect(page).to have_content user.email - expect(page).to_not have_content t('help_text.requested_attributes.address') - expect(page).to_not have_content t('help_text.requested_attributes.birthdate') - expect(page).to have_content t('help_text.requested_attributes.full_name') - expect(page).to have_content 'FAKEY MCFAKERSON' - expect(page).to have_content t('help_text.requested_attributes.phone') - expect(page).to have_content '+1 202-555-1212' - expect(page).to have_content t('help_text.requested_attributes.social_security_number') - expect(page).to have_css( - '.masked-text__text', - text: DocAuthHelper::GOOD_SSN, - visible: :hidden, - ) - end + expect(page).to have_content t('help_text.requested_attributes.email') + expect(page).to have_content user.email + expect(page).to_not have_content t('help_text.requested_attributes.address') + expect(page).to_not have_content t('help_text.requested_attributes.birthdate') + expect(page).to have_content t('help_text.requested_attributes.full_name') + expect(page).to have_content 'FAKEY MCFAKERSON' + expect(page).to have_content t('help_text.requested_attributes.phone') + expect(page).to have_content '+1 202-555-1212' + expect(page).to have_content t('help_text.requested_attributes.social_security_number') + expect(page).to have_css( + '.masked-text__text', + text: DocAuthHelper::GOOD_SSN, + visible: :hidden, + ) end end @@ -88,18 +86,16 @@ expect(current_path).to eq(sign_up_completed_path) - within('.requested-attributes') do - expect(page).to have_content t('help_text.requested_attributes.email') - expect(page).to have_content user.email - expect(page).to_not have_content t('help_text.requested_attributes.address') - expect(page).to_not have_content t('help_text.requested_attributes.birthdate') - expect(page).to have_content t('help_text.requested_attributes.full_name') - expect(page).to have_content 'FAKEY MCFAKERSON' - expect(page).to have_content t('help_text.requested_attributes.phone') - expect(page).to have_content '+1 202-555-1212' - expect(page).to have_content t('help_text.requested_attributes.social_security_number') - expect(page).to have_content DocAuthHelper::GOOD_SSN - end + expect(page).to have_content t('help_text.requested_attributes.email') + expect(page).to have_content user.email + expect(page).to_not have_content t('help_text.requested_attributes.address') + expect(page).to_not have_content t('help_text.requested_attributes.birthdate') + expect(page).to have_content t('help_text.requested_attributes.full_name') + expect(page).to have_content 'FAKEY MCFAKERSON' + expect(page).to have_content t('help_text.requested_attributes.phone') + expect(page).to have_content '+1 202-555-1212' + expect(page).to have_content t('help_text.requested_attributes.social_security_number') + expect(page).to have_content DocAuthHelper::GOOD_SSN end end end diff --git a/spec/views/idv/in_person/state_id.html.erb_spec.rb b/spec/views/idv/in_person/state_id.html.erb_spec.rb new file mode 100644 index 00000000000..f9be82b9844 --- /dev/null +++ b/spec/views/idv/in_person/state_id.html.erb_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +RSpec.describe 'idv/in_person/state_id.html.erb' do + let(:pii) { {} } + let(:form) { Idv::StateIdForm.new(pii) } + let(:parsed_dob) { Date.new(1970, 1, 1) } + + before do + allow(view).to receive(:url_for).and_return('https://example.com/') + end + + subject(:render_template) do + render template: 'idv/in_person/state_id', + locals: { updating_state_id: true, form: form, pii: pii, parsed_dob: parsed_dob } + end + + it 'renders state ID hint text with correct screenreader tags', aggregate_failures: true do + render_template + + doc = Nokogiri::HTML(rendered) + + jurisdiction_extras = doc.at_css('.jurisdiction-extras') + + all_hints = jurisdiction_extras.css('[data-state]') + shown = jurisdiction_extras.css('[data-state]:not(.display-none)') + hidden = jurisdiction_extras.css('[data-state].display-none') + + expect(shown.size).to eq(1), 'only shows one hint' + expect(shown.size + hidden.size).to eq(all_hints.size) + + default_hint = jurisdiction_extras.at_css('[data-state=default]') + default_hint_screenreader_tags = default_hint.css('.usa-sr-only') + *first, last = default_hint_screenreader_tags.map(&:text) + expect(first).to all end_with(',') + expect(last).to_not end_with(',') + end +end diff --git a/yarn.lock b/yarn.lock index 7c9e782c6e7..a77727f7c3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2936,11 +2936,6 @@ dirty-chai@^2.0.1: resolved "https://registry.yarnpkg.com/dirty-chai/-/dirty-chai-2.0.1.tgz#6b2162ef17f7943589da840abc96e75bda01aff3" integrity sha512-ys79pWKvDMowIDEPC6Fig8d5THiC0DJ2gmTeGzVAoEH18J8OzLud0Jh7I9IWg3NSk8x2UocznUuFmfHCXYZx9w== -dlv@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" - integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== - dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -4550,6 +4545,11 @@ known-css-properties@^0.27.0: resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.27.0.tgz#82a9358dda5fe7f7bd12b5e7142c0a205393c0c5" integrity sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg== +known-css-properties@^0.29.0: + version "0.29.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.29.0.tgz#e8ba024fb03886f23cb882e806929f32d814158f" + integrity sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ== + language-subtag-registry@~0.3.2: version "0.3.20" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz#a00a37121894f224f763268e431c55556b0c0755" @@ -5439,12 +5439,12 @@ postcss-safe-parser@^6.0.0: resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1" integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ== -postcss-scss@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.6.tgz#5d62a574b950a6ae12f2aa89b60d63d9e4432bfd" - integrity sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ== +postcss-scss@^4.0.9: + version "4.0.9" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.9.tgz#a03c773cd4c9623cb04ce142a52afcec74806685" + integrity sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A== -postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.13: +postcss-selector-parser@^6.0.13: version "6.0.13" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== @@ -5478,10 +5478,10 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" - integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== +prettier@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.0.tgz#c6d16474a5f764ea1a4a373c593b779697744d5e" + integrity sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw== pretty-format@^26.6.2: version "26.6.2" @@ -6382,19 +6382,19 @@ style-search@^0.1.0: resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" integrity sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI= -stylelint-config-recommended-scss@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-10.0.0.tgz#06c5c6ad893d2641d7207994de3a5aa2fdcb4078" - integrity sha512-+YvPgUHi0W5mCJCKdupBCIsWPYNbWuJcRmFtSYujwNg+41ljFknhO9bpY6C+oahv659zW7W1AT7i6DQvJYYr1A== +stylelint-config-recommended-scss@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-13.1.0.tgz#04e529ae0e9c1abb1e04de79258461c07811876f" + integrity sha512-8L5nDfd+YH6AOoBGKmhH8pLWF1dpfY816JtGMePcBqqSsLU+Ysawx44fQSlMOJ2xTfI9yTGpup5JU77c17w1Ww== dependencies: - postcss-scss "^4.0.6" - stylelint-config-recommended "^11.0.0" - stylelint-scss "^4.6.0" + postcss-scss "^4.0.9" + stylelint-config-recommended "^13.0.0" + stylelint-scss "^5.3.0" -stylelint-config-recommended@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-11.0.0.tgz#b1cb7d71bd92f9b8593f93c2ca6df16ed7d61522" - integrity sha512-SoGIHNI748OCZn6BxFYT83ytWoYETCINVHV3LKScVAWQQauWdvmdDqJC5YXWjpBbxg2E761Tg5aUGKLFOVhEkA== +stylelint-config-recommended@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-13.0.0.tgz#c48a358cc46b629ea01f22db60b351f703e00597" + integrity sha512-EH+yRj6h3GAe/fRiyaoO2F9l9Tgg50AOFhaszyfov9v6ayXJ1IkSHwTxd7lB48FmOeSGDPLjatjO11fJpmarkQ== stylelint-prettier@^4.0.2: version "4.0.2" @@ -6403,15 +6403,15 @@ stylelint-prettier@^4.0.2: dependencies: prettier-linter-helpers "^1.0.0" -stylelint-scss@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-4.6.0.tgz#f7602d6d562bb256802e38e3fd5e49c46d2e31b6" - integrity sha512-M+E0BQim6G4XEkaceEhfVjP/41C9Klg5/tTPTCQVlgw/jm2tvB+OXJGaU0TDP5rnTCB62aX6w+rT+gqJW/uwjA== +stylelint-scss@^5.3.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-5.3.1.tgz#7f0f5f06d0a2a3c515aa71d3a8de3548045e03e1" + integrity sha512-5I9ZDIm77BZrjOccma5WyW2nJEKjXDd4Ca8Kk+oBapSO4pewSlno3n+OyimcyVJJujQZkBN2D+xuMkIamSc6hA== dependencies: - dlv "^1.1.3" + known-css-properties "^0.29.0" postcss-media-query-parser "^0.2.3" postcss-resolve-nested-selector "^0.1.1" - postcss-selector-parser "^6.0.11" + postcss-selector-parser "^6.0.13" postcss-value-parser "^4.2.0" stylelint@^15.10.1: @@ -7155,10 +7155,10 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b" - integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== +yaml@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== yargs-parser@20.2.4: version "20.2.4"