diff --git a/app/controllers/account_reset/request_controller.rb b/app/controllers/account_reset/request_controller.rb index 05c18b926a4..0ecc00c604e 100644 --- a/app/controllers/account_reset/request_controller.rb +++ b/app/controllers/account_reset/request_controller.rb @@ -24,7 +24,7 @@ def create_account_reset_request def confirm_two_factor_enabled return if MfaPolicy.new(current_user).two_factor_enabled? - redirect_to two_factor_options_url + redirect_to authentication_methods_setup_url end def analytics_attributes diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb deleted file mode 100644 index 5248571b1a2..00000000000 --- a/app/controllers/api/base_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Api - class BaseController < ApplicationController - include RenderConditionConcern - - check_or_render_not_found -> { FeatureManagement.idv_api_enabled? } - before_action :confirm_two_factor_authenticated_for_api - - respond_to :json - - def confirm_two_factor_authenticated_for_api - return if user_fully_authenticated? - render json: { error: 'user is not fully authenticated' }, status: :unauthorized - end - end -end diff --git a/app/controllers/api/verify/base_controller.rb b/app/controllers/api/verify/base_controller.rb new file mode 100644 index 00000000000..2b54c602cf0 --- /dev/null +++ b/app/controllers/api/verify/base_controller.rb @@ -0,0 +1,26 @@ +module Api + module Verify + class BaseController < ApplicationController + include RenderConditionConcern + + class_attribute :required_step + + check_or_render_not_found -> do + if self.class.required_step.blank? + raise NotImplementedError, 'Controller must define required_step' + end + IdentityConfig.store.idv_api_enabled_steps.include?(self.class.required_step) + end + before_action :confirm_two_factor_authenticated_for_api + + respond_to :json + + private + + def confirm_two_factor_authenticated_for_api + return if user_fully_authenticated? + render json: { error: 'user is not fully authenticated' }, status: :unauthorized + end + end + end +end diff --git a/app/controllers/api/verify/password_confirm_controller.rb b/app/controllers/api/verify/password_confirm_controller.rb index d05f2f40e4c..19bd41c51ee 100644 --- a/app/controllers/api/verify/password_confirm_controller.rb +++ b/app/controllers/api/verify/password_confirm_controller.rb @@ -1,6 +1,8 @@ module Api module Verify - class PasswordConfirmController < Api::BaseController + class PasswordConfirmController < BaseController + self.required_step = 'password_confirm' + def create result, personal_key = Api::ProfileCreationForm.new( password: verify_params[:password], @@ -12,9 +14,10 @@ def create if result.success? user = User.find_by(uuid: result.extra[:user_uuid]) add_proofing_component(user) - render json: { personal_key: personal_key, - profile_pending: result.extra[:profile_pending] }, - status: :ok + render json: { + personal_key: personal_key, + completion_url: completion_url(result), + } else render json: { error: result.errors }, status: :bad_request end @@ -29,6 +32,16 @@ def verify_params def add_proofing_component(user) ProofingComponent.create_or_find_by(user: user).update(verified_at: Time.zone.now) end + + def completion_url(result) + if result.extra[:profile_pending] + idv_come_back_later_url + elsif current_sp + sign_up_completed_url + else + account_url + end + end end end end diff --git a/app/controllers/api/verify/password_reset_controller.rb b/app/controllers/api/verify/password_reset_controller.rb index a480eed117a..4fb2be30dd4 100644 --- a/app/controllers/api/verify/password_reset_controller.rb +++ b/app/controllers/api/verify/password_reset_controller.rb @@ -1,6 +1,8 @@ module Api module Verify - class PasswordResetController < Api::BaseController + class PasswordResetController < BaseController + self.required_step = 'password_confirm' + def create analytics.idv_forgot_password_confirmed request_id = sp_session[:request_id] diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 830d7596552..59707cdce32 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -186,7 +186,8 @@ def add_piv_cac_setup_url end def service_provider_mfa_setup_url - service_provider_mfa_policy.user_needs_sp_auth_method_setup? ? two_factor_options_url : nil + service_provider_mfa_policy.user_needs_sp_auth_method_setup? ? + authentication_methods_setup_url : nil end def fix_broken_personal_key_url @@ -312,7 +313,7 @@ def prompt_to_sign_in_with_request_id(request_id) end def prompt_to_setup_mfa - redirect_to two_factor_options_url + redirect_to authentication_methods_setup_url end def prompt_to_verify_mfa diff --git a/app/controllers/concerns/render_condition_concern.rb b/app/controllers/concerns/render_condition_concern.rb index e68fcfaebc0..e2d69c37690 100644 --- a/app/controllers/concerns/render_condition_concern.rb +++ b/app/controllers/concerns/render_condition_concern.rb @@ -3,7 +3,7 @@ module RenderConditionConcern module ClassMethods def check_or_render_not_found(callable, **kwargs) - before_action(**kwargs) { render_not_found if !callable.call } + before_action(**kwargs) { render_not_found if !instance_exec(&callable) } end end end diff --git a/app/controllers/concerns/two_factor_authenticatable_methods.rb b/app/controllers/concerns/two_factor_authenticatable_methods.rb index 7cfc8442e4a..6f773ae6ca5 100644 --- a/app/controllers/concerns/two_factor_authenticatable_methods.rb +++ b/app/controllers/concerns/two_factor_authenticatable_methods.rb @@ -18,14 +18,14 @@ def authenticate_user end def handle_second_factor_locked_user(type) - analytics.track_event(Analytics::MULTI_FACTOR_AUTH_MAX_ATTEMPTS) + analytics.multi_factor_auth_max_attempts event = PushNotification::MfaLimitAccountLockedEvent.new(user: current_user) PushNotification::HttpPush.deliver(event) handle_max_attempts(type + '_login_attempts') end def handle_too_many_otp_sends - analytics.track_event(Analytics::MULTI_FACTOR_AUTH_MAX_SENDS) + analytics.multi_factor_auth_max_sends handle_max_attempts('otp_requests') end diff --git a/app/controllers/idv/review_controller.rb b/app/controllers/idv/review_controller.rb index b451c974572..78fa290f07a 100644 --- a/app/controllers/idv/review_controller.rb +++ b/app/controllers/idv/review_controller.rb @@ -120,16 +120,7 @@ def need_personal_key_confirmation? end def next_step - if idv_api_personal_key_step_enabled? - idv_app_url - else - idv_personal_key_url - end - end - - def idv_api_personal_key_step_enabled? - return false if idv_session.address_verification_mechanism == 'gpo' - IdentityConfig.store.idv_api_enabled_steps.include?('personal_key') + idv_personal_key_url end end end diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index 0040e9683da..906f74d359b 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -83,9 +83,7 @@ def track_authorize_analytics(result) analytics_attributes = result.to_h.except(:redirect_uri). merge(user_fully_authenticated: user_fully_authenticated?) - analytics.track_event( - Analytics::OPENID_CONNECT_REQUEST_AUTHORIZATION, analytics_attributes - ) + analytics.openid_connect_request_authorization(**analytics_attributes) end def identity_needs_verification? diff --git a/app/controllers/openid_connect/token_controller.rb b/app/controllers/openid_connect/token_controller.rb index 4dd97b29e50..92299cac934 100644 --- a/app/controllers/openid_connect/token_controller.rb +++ b/app/controllers/openid_connect/token_controller.rb @@ -8,7 +8,7 @@ def create @token_form = OpenidConnectTokenForm.new(token_params) result = @token_form.submit - analytics.track_event(Analytics::OPENID_CONNECT_TOKEN, result.to_h) + analytics.openid_connect_token(**result.to_h) render json: @token_form.response, status: (result.success? ? :ok : :bad_request) diff --git a/app/controllers/openid_connect/user_info_controller.rb b/app/controllers/openid_connect/user_info_controller.rb index 3d5a9b34b47..23c36b1b022 100644 --- a/app/controllers/openid_connect/user_info_controller.rb +++ b/app/controllers/openid_connect/user_info_controller.rb @@ -16,7 +16,7 @@ def show def authenticate_identity_via_bearer_token verifier = AccessTokenVerifier.new(request.env['HTTP_AUTHORIZATION']) response = verifier.submit - analytics.track_event(Analytics::OPENID_CONNECT_BEARER_TOKEN, response.to_h) + analytics.openid_connect_bearer_token(**response.to_h) if response.success? @current_identity = verifier.identity diff --git a/app/controllers/sign_up/passwords_controller.rb b/app/controllers/sign_up/passwords_controller.rb index b5699926e3a..9441308eec8 100644 --- a/app/controllers/sign_up/passwords_controller.rb +++ b/app/controllers/sign_up/passwords_controller.rb @@ -75,7 +75,7 @@ def process_unsuccessful_password_creation def sign_in_and_redirect_user sign_in @user - redirect_to two_factor_options_url + redirect_to authentication_methods_setup_url end end end diff --git a/app/controllers/two_factor_authentication/options_controller.rb b/app/controllers/two_factor_authentication/options_controller.rb index 45088f1d816..ad53b75b695 100644 --- a/app/controllers/two_factor_authentication/options_controller.rb +++ b/app/controllers/two_factor_authentication/options_controller.rb @@ -26,13 +26,13 @@ class OptionsController < ApplicationController def index @two_factor_options_form = TwoFactorLoginOptionsForm.new(current_user) @presenter = two_factor_options_presenter - analytics.track_event(Analytics::MULTI_FACTOR_AUTH_OPTION_LIST_VISIT) + analytics.multi_factor_auth_option_list_visit end def create @two_factor_options_form = TwoFactorLoginOptionsForm.new(current_user) result = @two_factor_options_form.submit(two_factor_options_form_params) - analytics.track_event(Analytics::MULTI_FACTOR_AUTH_OPTION_LIST, result.to_h) + analytics.multi_factor_auth_option_list(**result.to_h) if result.success? process_valid_form diff --git a/app/controllers/two_factor_authentication/otp_verification_controller.rb b/app/controllers/two_factor_authentication/otp_verification_controller.rb index 06e45f4ed28..c6c10a3ef2f 100644 --- a/app/controllers/two_factor_authentication/otp_verification_controller.rb +++ b/app/controllers/two_factor_authentication/otp_verification_controller.rb @@ -78,9 +78,7 @@ def form_params def post_analytics(result) properties = result.to_h.merge(analytics_properties) - if context == 'confirmation' - analytics.track_event(Analytics::MULTI_FACTOR_AUTH_SETUP, properties) - end + analytics.multi_factor_auth_setup(**properties) if context == 'confirmation' analytics.track_mfa_submit_event(properties) end 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 eb56cad8773..9237f4bab0e 100644 --- a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb +++ b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb @@ -26,7 +26,7 @@ def create def check_personal_key_enabled return if TwoFactorAuthentication::PersonalKeyPolicy.new(current_user).enabled? - redirect_to two_factor_options_url + redirect_to authentication_methods_setup_url end def presenter_for_two_factor_authentication_method @@ -97,7 +97,7 @@ def handle_valid_otp elsif MfaPolicy.new(current_user).two_factor_enabled? redirect_to after_mfa_setup_path else - redirect_to two_factor_options_url + redirect_to authentication_methods_setup_url end reset_otp_session_data end diff --git a/app/controllers/two_factor_authentication/sms_opt_in_controller.rb b/app/controllers/two_factor_authentication/sms_opt_in_controller.rb index ea324fc7030..2b66d632443 100644 --- a/app/controllers/two_factor_authentication/sms_opt_in_controller.rb +++ b/app/controllers/two_factor_authentication/sms_opt_in_controller.rb @@ -65,7 +65,7 @@ def load_phone def other_options_mfa_url if new_user? - two_factor_options_path + authentication_methods_setup_path elsif has_other_auth_methods? && !user_fully_authenticated? login_two_factor_options_path end diff --git a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb index 7d0df1f6adb..94bbd395a64 100644 --- a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb +++ b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb @@ -100,7 +100,8 @@ def credential_ids end def analytics_properties - auth_method = if form&.webauthn_configuration&.platform_authenticator || params[:platform] + auth_method = if form&.webauthn_configuration&.platform_authenticator || + params[:platform].to_s == 'true' 'webauthn_platform' else 'webauthn' diff --git a/app/controllers/users/mfa_selection_controller.rb b/app/controllers/users/mfa_selection_controller.rb index 6a0cca2e26c..0cfc76e1b09 100644 --- a/app/controllers/users/mfa_selection_controller.rb +++ b/app/controllers/users/mfa_selection_controller.rb @@ -9,6 +9,7 @@ class MfaSelectionController < ApplicationController def index @two_factor_options_form = TwoFactorOptionsForm.new(current_user) + @after_setup_path = after_mfa_setup_path @presenter = two_factor_options_presenter analytics.user_registration_2fa_additional_setup_visit end diff --git a/app/controllers/users/phone_setup_controller.rb b/app/controllers/users/phone_setup_controller.rb index 5cdc0548b76..907f1ff6e37 100644 --- a/app/controllers/users/phone_setup_controller.rb +++ b/app/controllers/users/phone_setup_controller.rb @@ -16,7 +16,7 @@ def index def create @new_phone_form = NewPhoneForm.new(current_user) result = @new_phone_form.submit(new_phone_form_params) - analytics.track_event(Analytics::MULTI_FACTOR_AUTH_PHONE_SETUP, result.to_h) + analytics.multi_factor_auth_phone_setup(**result.to_h) if result.success? handle_create_success(@new_phone_form.phone) diff --git a/app/controllers/users/piv_cac_authentication_setup_controller.rb b/app/controllers/users/piv_cac_authentication_setup_controller.rb index 741eee77bfa..20e27bc76d9 100644 --- a/app/controllers/users/piv_cac_authentication_setup_controller.rb +++ b/app/controllers/users/piv_cac_authentication_setup_controller.rb @@ -75,7 +75,7 @@ def piv_cac_service_url_with_redirect def process_piv_cac_setup result = user_piv_cac_form.submit - analytics.track_event(Analytics::MULTI_FACTOR_AUTH_SETUP, result.to_h) + analytics.multi_factor_auth_setup(**result.to_h) if result.success? process_valid_submission else 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 04b1511232a..85b42f70c99 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 @@ -35,7 +35,7 @@ def render_prompt def process_piv_cac_setup result = user_piv_cac_form.submit - analytics.track_event(Analytics::MULTI_FACTOR_AUTH_SETUP, result.to_h) + analytics.multi_factor_auth_setup(**result.to_h) if result.success? process_valid_submission else diff --git a/app/controllers/users/totp_setup_controller.rb b/app/controllers/users/totp_setup_controller.rb index 2a4454b830b..ccce1677efe 100644 --- a/app/controllers/users/totp_setup_controller.rb +++ b/app/controllers/users/totp_setup_controller.rb @@ -22,7 +22,7 @@ def new def confirm result = totp_setup_form.submit - analytics.track_event(Analytics::MULTI_FACTOR_AUTH_SETUP, result.to_h) + analytics.multi_factor_auth_setup(**result.to_h) if result.success? process_valid_code diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb index 2101e98867c..55006ae0373 100644 --- a/app/controllers/users/two_factor_authentication_controller.rb +++ b/app/controllers/users/two_factor_authentication_controller.rb @@ -14,7 +14,7 @@ def show def send_code result = otp_delivery_selection_form.submit(delivery_params) - analytics.track_event(Analytics::OTP_DELIVERY_SELECTION, result.to_h) + analytics.otp_delivery_selection(**result.to_h) if result.success? handle_valid_otp_params(user_select_delivery_preference, user_selected_default_number) update_otp_delivery_preference_if_needed @@ -54,7 +54,7 @@ def redirect_on_nothing_enabled if MfaPolicy.new(current_user).two_factor_enabled? redirect_to login_two_factor_options_path else - redirect_to two_factor_options_url + redirect_to authentication_methods_setup_url end end @@ -68,7 +68,7 @@ def phone_configuration def validate_otp_delivery_preference_and_send_code result = otp_delivery_selection_form.submit(otp_delivery_preference: delivery_preference) - analytics.track_event(Analytics::OTP_DELIVERY_SELECTION, result.to_h) + analytics.otp_delivery_selection(**result.to_h) phone_is_confirmed = UserSessionContext.authentication_context?(context) phone_capabilities = PhoneNumberCapabilities.new( parsed_phone, @@ -180,7 +180,7 @@ def handle_valid_otp_params(method, default = nil) end def handle_telephony_result(method:, default:) - track_events + track_events(otp_delivery_preference: method) if @telephony_result.success? redirect_to login_two_factor_url( otp_delivery_preference: method, @@ -197,8 +197,17 @@ def handle_telephony_result(method:, default:) end end - def track_events - analytics.track_event(Analytics::TELEPHONY_OTP_SENT, @telephony_result.to_h) + def track_events(otp_delivery_preference:) + analytics.telephony_otp_sent( + area_code: parsed_phone.area_code, + country_code: parsed_phone.country_code, + phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164), + context: context, + otp_delivery_preference: otp_delivery_preference, + resend: params.dig(:otp_delivery_selection_form, :resend), + telephony_response: @telephony_result.to_h, + success: @telephony_result.success?, + ) end def exceeded_otp_send_limit? @@ -308,7 +317,7 @@ def handle_too_many_confirmation_sends if user_fully_authenticated? redirect_to account_url else - redirect_to two_factor_options_url + redirect_to authentication_methods_setup_url end end end diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb index 0ed30946a07..2177883bc12 100644 --- a/app/controllers/users/two_factor_authentication_setup_controller.rb +++ b/app/controllers/users/two_factor_authentication_setup_controller.rb @@ -22,14 +22,14 @@ def create elsif (result.errors[:selection].include? 'phone') || IdentityConfig.store.kantara_2fa_phone_restricted flash[:phone_error] = t('errors.two_factor_auth_setup.must_select_additional_option') - redirect_to two_factor_options_path(anchor: 'select_phone') + redirect_to authentication_methods_setup_path(anchor: 'select_phone') else @presenter = two_factor_options_presenter render :index end rescue ActionController::ParameterMissing flash[:error] = t('errors.two_factor_auth_setup.must_select_option') - redirect_back(fallback_location: two_factor_options_path, allow_other_host: false) + redirect_back(fallback_location: authentication_methods_setup_path, allow_other_host: false) end private diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index 1da5870cc90..85c112c64c6 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -37,7 +37,7 @@ def confirm remember_device_default: remember_device_default, platform_authenticator: @platform_authenticator, ) - analytics.track_event(Analytics::MULTI_FACTOR_AUTH_SETUP, result.to_h) + analytics.multi_factor_auth_setup(**result.to_h) if result.success? process_valid_webauthn(form) else diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb deleted file mode 100644 index 10992662a80..00000000000 --- a/app/controllers/users_controller.rb +++ /dev/null @@ -1,32 +0,0 @@ -class UsersController < ApplicationController - before_action :ensure_in_setup - - def destroy - track_account_deletion_event - url_after_cancellation = decorated_session.cancel_link_url - destroy_user - flash[:success] = t('sign_up.cancel.success') - redirect_to url_after_cancellation - end - - private - - def track_account_deletion_event - properties = ParseControllerFromReferer.new(request.referer).call - analytics.account_deletion(**properties) - end - - def destroy_user - user = current_user || User.find_by(confirmation_token: session[:user_confirmation_token]) - user&.destroy! - sign_out if user - end - - def ensure_in_setup - redirect_to root_url if !session[:user_confirmation_token] && two_factor_enabled - end - - def two_factor_enabled - current_user && MfaPolicy.new(current_user).two_factor_enabled? - end -end diff --git a/app/controllers/verify_controller.rb b/app/controllers/verify_controller.rb index b9eebf22b7b..f172fbe3eb7 100644 --- a/app/controllers/verify_controller.rb +++ b/app/controllers/verify_controller.rb @@ -7,7 +7,6 @@ class VerifyController < ApplicationController before_action :validate_step before_action :confirm_two_factor_authenticated before_action :confirm_idv_vendor_session_started - before_action :confirm_profile_has_been_created, if: :first_step_is_personal_key? def show @app_data = app_data @@ -26,7 +25,6 @@ def app_data base_path: idv_app_path, start_over_url: idv_session_path, cancel_url: idv_cancel_path, - completion_url: completion_url, initial_values: initial_values, reset_password_url: forgot_password_url, enabled_step_names: IdentityConfig.store.idv_api_enabled_steps, @@ -38,8 +36,6 @@ def initial_values case first_step when 'password_confirm' { 'userBundleToken' => user_bundle_token } - when 'personal_key' - { 'personalKey' => personal_key } end end @@ -47,10 +43,6 @@ def first_step enabled_steps.detect { |step| step_enabled?(step) } end - def first_step_is_personal_key? - first_step == 'personal_key' - end - def enabled_steps IdentityConfig.store.idv_api_enabled_steps end @@ -63,27 +55,6 @@ def random_encryption_key Encryption::AesCipher.encryption_cipher.random_key end - def confirm_profile_has_been_created - redirect_to account_url if idv_session.profile.blank? - end - - def personal_key - idv_session.personal_key || generate_personal_key - end - - def generate_personal_key - cacher = Pii::Cacher.new(current_user, user_session) - idv_session.profile.encrypt_recovery_pii(cacher.fetch) - end - - def completion_url - if session[:sp] - sign_up_completed_url - else - after_sign_in_path_for(current_user) - end - end - def user_bundle_token Idv::UserBundleTokenizer.new( user: current_user, diff --git a/app/decorators/user_decorator.rb b/app/decorators/user_decorator.rb index 5245f618991..b709f1af0ec 100644 --- a/app/decorators/user_decorator.rb +++ b/app/decorators/user_decorator.rb @@ -73,7 +73,7 @@ def active_profile_newer_than_pending_profile? # This user's most recently activated profile that has also been deactivated # due to a password reset, or nil if there is no such profile def password_reset_profile - profile = user.profiles.order(activated_at: :desc).first + profile = user.profiles.where.not(activated_at: nil).order(activated_at: :desc).first profile if profile&.password_reset? end diff --git a/app/javascript/packages/components/index.ts b/app/javascript/packages/components/index.ts index 4a591e100be..60715ffb7cb 100644 --- a/app/javascript/packages/components/index.ts +++ b/app/javascript/packages/components/index.ts @@ -7,6 +7,7 @@ export { default as Icon } from './icon'; export { default as FullScreen } from './full-screen'; export { default as Link } from './link'; export { default as PageHeading } from './page-heading'; +export { default as ScrollIntoView } from './scroll-into-view'; export { default as SpinnerDots } from './spinner-dots'; export { default as TextInput } from './text-input'; export { default as TroubleshootingOptions } from './troubleshooting-options'; diff --git a/app/javascript/packages/components/scroll-into-view.spec.tsx b/app/javascript/packages/components/scroll-into-view.spec.tsx new file mode 100644 index 00000000000..97e44e665ff --- /dev/null +++ b/app/javascript/packages/components/scroll-into-view.spec.tsx @@ -0,0 +1,28 @@ +import { render } from '@testing-library/react'; +import type { SinonSpy } from 'sinon'; +import { useSandbox } from '@18f/identity-test-helpers'; +import ScrollIntoView from './scroll-into-view'; + +describe('ScrollIntoView', () => { + const sandbox = useSandbox(); + + it('scrolls content into view', () => { + sandbox.spy(Element.prototype, 'scrollIntoView'); + + const { getByText } = render(Content); + + const text = getByText('Content'); + const call = (Element.prototype.scrollIntoView as SinonSpy).getCall(0); + + expect((call.thisValue as Element).contains(text)).to.be.true(); + }); + + it('only scrolls into view on initial render', () => { + sandbox.spy(Element.prototype, 'scrollIntoView'); + + const { rerender } = render(Content1); + rerender(Content2); + + expect(Element.prototype.scrollIntoView).to.have.been.calledOnce(); + }); +}); diff --git a/app/javascript/packages/components/scroll-into-view.tsx b/app/javascript/packages/components/scroll-into-view.tsx new file mode 100644 index 00000000000..dfc3853136f --- /dev/null +++ b/app/javascript/packages/components/scroll-into-view.tsx @@ -0,0 +1,18 @@ +import { useRef, useEffect } from 'react'; +import type { ReactNode } from 'react'; + +interface ScrollIntoViewProps { + children: ReactNode; +} + +/** + * Scrolls content into the user's viewport when mounted. + */ +function ScrollIntoView({ children }: ScrollIntoViewProps) { + const ref = useRef(null); + useEffect(() => ref.current?.scrollIntoView(), []); + + return
{children}
; +} + +export default ScrollIntoView; diff --git a/app/javascript/packages/verify-flow/context/address-verification-method-context.spec.tsx b/app/javascript/packages/verify-flow/context/address-verification-method-context.spec.tsx new file mode 100644 index 00000000000..b51b41b20eb --- /dev/null +++ b/app/javascript/packages/verify-flow/context/address-verification-method-context.spec.tsx @@ -0,0 +1,59 @@ +import { useContext } from 'react'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import AddressVerificationMethodContext, { + AddressVerificationMethodContextProvider, +} from './address-verification-method-context'; + +describe('AddressVerificationMethodContextProvider', () => { + function TestComponent() { + const { addressVerificationMethod, setAddressVerificationMethod } = useContext( + AddressVerificationMethodContext, + ); + return ( + <> +
Current value: {addressVerificationMethod}
+ + + ); + } + + it('initializes with default value', () => { + const { getByText } = render( + + + , + ); + + expect(getByText('Current value: phone')).to.exist(); + }); + + it('can be overridden with an initial method', () => { + const { getByText } = render( + + + , + ); + + expect(getByText('Current value: gpo')).to.exist(); + }); + + it('exposes a setter to change the value', async () => { + const { getByText } = render( + + + , + ); + + await userEvent.click(getByText('Update')); + + expect(getByText('Current value: phone')).to.exist(); + }); +}); diff --git a/app/javascript/packages/verify-flow/context/address-verification-method-context.tsx b/app/javascript/packages/verify-flow/context/address-verification-method-context.tsx new file mode 100644 index 00000000000..66a1a7d092a --- /dev/null +++ b/app/javascript/packages/verify-flow/context/address-verification-method-context.tsx @@ -0,0 +1,69 @@ +import { createContext, useState } from 'react'; +import { useObjectMemo } from '@18f/identity-react-hooks'; +import type { ReactNode } from 'react'; + +/** + * Mechanisms by which a user can verify their address. + */ +export type AddressVerificationMethod = 'phone' | 'gpo'; + +/** + * Context provider props. + */ +interface AddressVerificationMethodContextProviderProps { + /** + * Optional initial context value. + */ + initialMethod?: AddressVerificationMethod; + + /** + * Context children. + */ + children?: ReactNode; +} + +/** + * Context value. + */ +interface AddressVerificationMethodContextValue { + /** + * Current address verification method. + */ + addressVerificationMethod: AddressVerificationMethod; + + /** + * Setter to update to a new address verification method. + */ + setAddressVerificationMethod: (nextMethod: AddressVerificationMethod) => void; +} + +/** + * Default address verification method. + */ +const DEFAULT_METHOD: AddressVerificationMethod = 'phone'; + +/** + * Address verification method context container. + */ +const AddressVerificationMethodContext = createContext({ + addressVerificationMethod: DEFAULT_METHOD, + setAddressVerificationMethod: () => {}, +}); + +AddressVerificationMethodContext.displayName = 'AddressVerificationMethodContext'; + +export function AddressVerificationMethodContextProvider({ + initialMethod = DEFAULT_METHOD, + children, +}: AddressVerificationMethodContextProviderProps) { + const [addressVerificationMethod, setAddressVerificationMethod] = useState(initialMethod); + const value = useObjectMemo({ addressVerificationMethod, setAddressVerificationMethod }); + + return ( + + {children} + + ); +} + +export default AddressVerificationMethodContext; diff --git a/app/javascript/packages/verify-flow/index.ts b/app/javascript/packages/verify-flow/index.ts index 26ca86f8c73..a4d10d989ad 100644 --- a/app/javascript/packages/verify-flow/index.ts +++ b/app/javascript/packages/verify-flow/index.ts @@ -4,4 +4,5 @@ export { default as StartOverOrCancel } from './start-over-or-cancel'; export { default as VerifyFlow } from './verify-flow'; export type { SecretValues } from './context/secrets-context'; +export type { AddressVerificationMethod } from './context/address-verification-method-context'; export type { VerifyFlowValues } from './verify-flow'; diff --git a/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.spec.tsx b/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.spec.tsx index 22d0c9a1007..05fde8acf62 100644 --- a/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.spec.tsx +++ b/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.spec.tsx @@ -2,12 +2,14 @@ import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { computeAccessibleDescription } from 'dom-accessibility-api'; import { accordion } from 'identity-style-guide'; +import type { SinonSpy } from 'sinon'; import * as analytics from '@18f/identity-analytics'; import { useSandbox, usePropertyValue } from '@18f/identity-test-helpers'; import { FormSteps } from '@18f/identity-form-steps'; import { t, i18n } from '@18f/identity-i18n'; import PasswordConfirmStep from './password-confirm-step'; import submit, { PasswordSubmitError } from './submit'; +import { AddressVerificationMethodContextProvider } from '../../context/address-verification-method-context'; describe('PasswordConfirmStep', () => { const sandbox = useSandbox(); @@ -82,15 +84,24 @@ describe('PasswordConfirmStep', () => { , ); + sandbox.spy(Element.prototype, 'scrollIntoView'); + const continueButton = getByRole('button', { name: 'forms.buttons.continue' }); + await userEvent.type(getByLabelText('components.password_toggle.label'), 'password'); - await userEvent.click(getByRole('button', { name: 'forms.buttons.continue' })); + await userEvent.click(continueButton); // There should not be a field-specific error, only a top-level alert. const alert = await findByRole('alert'); + expect(Element.prototype.scrollIntoView).to.have.been.calledOnce(); + const { thisValue: scrollElement } = (Element.prototype.scrollIntoView as SinonSpy).getCall(0); + expect((scrollElement as Element).contains(alert)).to.be.true(); expect(alert.textContent).to.equal('Incorrect password'); const input = getByLabelText('components.password_toggle.label'); const description = computeAccessibleDescription(input); expect(description).to.be.empty(); + + await userEvent.click(continueButton); + expect(Element.prototype.scrollIntoView).to.have.been.calledTwice(); }); describe('forgot password', () => { @@ -118,18 +129,24 @@ describe('PasswordConfirmStep', () => { }); describe('alert', () => { - context('without phone value', () => { + context('with gpo as address verification method', () => { it('does not render success alert', () => { - const { queryByRole } = render(); + const { queryByRole } = render( + + + , + ); expect(queryByRole('status')).to.not.exist(); }); }); - context('with phone value', () => { + context('with phone as address verification method', () => { it('renders success alert', () => { const { queryByRole } = render( - , + + + , ); const status = queryByRole('status')!; diff --git a/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.tsx b/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.tsx index c51531e2cc5..8f6d566b114 100644 --- a/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.tsx +++ b/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.tsx @@ -10,13 +10,14 @@ import { import { PasswordToggle } from '@18f/identity-password-toggle'; import { FlowContext } from '@18f/identity-verify-flow'; import { formatHTML } from '@18f/identity-react-i18n'; -import { PageHeading, Accordion, Alert, Link } from '@18f/identity-components'; +import { PageHeading, Accordion, Alert, Link, ScrollIntoView } from '@18f/identity-components'; import { getConfigValue } from '@18f/identity-config'; import type { ChangeEvent } from 'react'; import type { FormStepComponentProps } from '@18f/identity-form-steps'; import { ForgotPassword } from './forgot-password'; import PersonalInfoSummary from './personal-info-summary'; import StartOverOrCancel from '../../start-over-or-cancel'; +import AddressVerificationMethodContext from '../../context/address-verification-method-context'; import type { VerifyFlowValues } from '../..'; import { PasswordSubmitError } from './submit'; @@ -27,6 +28,7 @@ const FORGOT_PASSWORD_PATH = 'forgot_password'; function PasswordConfirmStep({ errors, registerField, onChange, value }: PasswordConfirmStepProps) { const { basePath } = useContext(FlowContext); const { onPageTransition } = useContext(FormStepsContext); + const { addressVerificationMethod } = useContext(AddressVerificationMethodContext); const stepPath = `${basePath}/password_confirm`; const [path] = useHistoryParam(undefined, { basePath: stepPath }); useDidUpdateEffect(onPageTransition, [path]); @@ -36,10 +38,11 @@ function PasswordConfirmStep({ errors, registerField, onChange, value }: Passwor } const appName = getConfigValue('appName'); + const stepErrors = errors.filter(({ error }) => error instanceof PasswordSubmitError); return ( <> - {value.phone && ( + {addressVerificationMethod === 'phone' && ( {formatHTML( t('idv.messages.review.info_verified_html', { @@ -49,13 +52,15 @@ function PasswordConfirmStep({ errors, registerField, onChange, value }: Passwor )} )} - {errors - .filter(({ error }) => error instanceof PasswordSubmitError) - .map(({ error }) => ( - - {error.message} - - ))} + {stepErrors.length > 0 && ( + + {stepErrors.map(({ error }) => ( + + {error.message} + + ))} + + )} {t('idv.titles.session.review', { app_name: appName })}

{t('idv.messages.sessions.review_message', { app_name: appName })}

diff --git a/app/javascript/packages/verify-flow/steps/password-confirm/submit.spec.ts b/app/javascript/packages/verify-flow/steps/password-confirm/submit.spec.ts index 8238940081c..ebf6d59c81b 100644 --- a/app/javascript/packages/verify-flow/steps/password-confirm/submit.spec.ts +++ b/app/javascript/packages/verify-flow/steps/password-confirm/submit.spec.ts @@ -14,14 +14,21 @@ describe('submit', () => { sandbox.match({ body: JSON.stringify({ user_bundle_token: '..', password: 'hunter2' }) }), ) .resolves({ - json: () => Promise.resolve({ personal_key: '0000-0000-0000-0000' }), + json: () => + Promise.resolve({ + personal_key: '0000-0000-0000-0000', + completion_url: 'http://example.com', + }), } as Response); }); it('sends with password confirmation values', async () => { const patch = await submit({ userBundleToken: '..', password: 'hunter2' }); - expect(patch).to.deep.equal({ personalKey: '0000-0000-0000-0000' }); + expect(patch).to.deep.equal({ + personalKey: '0000-0000-0000-0000', + completionURL: 'http://example.com', + }); }); }); diff --git a/app/javascript/packages/verify-flow/steps/password-confirm/submit.ts b/app/javascript/packages/verify-flow/steps/password-confirm/submit.ts index 0212e28b1f9..34db4f51a6f 100644 --- a/app/javascript/packages/verify-flow/steps/password-confirm/submit.ts +++ b/app/javascript/packages/verify-flow/steps/password-confirm/submit.ts @@ -13,7 +13,15 @@ export class PasswordSubmitError extends FormError {} * Successful API response shape. */ interface PasswordConfirmSuccessResponse { + /** + * Personal key generated for the user profile. + */ personal_key: string; + + /** + * Final redirect URL for this verification session. + */ + completion_url: string; } /** @@ -26,7 +34,10 @@ type PasswordConfirmErrorResponse = ErrorResponse<'password'>; */ type PasswordConfirmResponse = PasswordConfirmSuccessResponse | PasswordConfirmErrorResponse; -async function submit({ userBundleToken, password }: VerifyFlowValues) { +async function submit({ + userBundleToken, + password, +}: VerifyFlowValues): Promise> { const payload = { user_bundle_token: userBundleToken, password }; const json = await post(API_ENDPOINT, payload, { json: true, @@ -38,7 +49,7 @@ async function submit({ userBundleToken, password }: VerifyFlowValues) { throw new PasswordSubmitError(error, { field }); } - return { personalKey: json.personal_key }; + return { personalKey: json.personal_key, completionURL: json.completion_url }; } export default submit; diff --git a/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.spec.tsx b/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.spec.tsx index 828de0d5291..67616a4a328 100644 --- a/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.spec.tsx +++ b/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.spec.tsx @@ -49,6 +49,17 @@ describe('PersonalKeyInput', () => { expect(input.value).to.equal('1234-1234-1234-1234'); }); + it('allows the user to paste the personal key from their clipboard', async () => { + const { getByRole } = render(); + + const input = getByRole('textbox') as HTMLInputElement; + + input.focus(); + await userEvent.paste('1234-1234-1234-1234'); + + expect(input.value).to.equal('1234-1234-1234-1234'); + }); + it('validates the input value against the expected value (case-insensitive, crockford)', async () => { const { getByRole } = render(); diff --git a/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.tsx b/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.tsx index 03d2a835c3f..21b7bde05a0 100644 --- a/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.tsx +++ b/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.tsx @@ -1,10 +1,18 @@ import { forwardRef, useCallback } from 'react'; import type { ForwardedRef } from 'react'; import Cleave from 'cleave.js/react'; +import type { ReactInstanceWithCleave } from 'cleave.js/react/props'; import { t } from '@18f/identity-i18n'; import { ValidatedField } from '@18f/identity-validated-field'; import type { ValidatedFieldValidator } from '@18f/identity-validated-field'; +/** + * Internal Cleave.js React instance API methods. + */ +interface CleaveInstanceInternalAPI { + updateValueState: () => void; +} + interface PersonalKeyInputProps { /** * The correct personal key to validate against. @@ -42,6 +50,9 @@ function PersonalKeyInput( return ( { + (owner as ReactInstanceWithCleave & CleaveInstanceInternalAPI).updateValueState(); + }} options={{ blocks: [4, 4, 4, 4], delimiter: '-', diff --git a/app/javascript/packages/verify-flow/steps/personal-key/personal-key-step.spec.tsx b/app/javascript/packages/verify-flow/steps/personal-key/personal-key-step.spec.tsx index 9536b7d6116..d4a207065fe 100644 --- a/app/javascript/packages/verify-flow/steps/personal-key/personal-key-step.spec.tsx +++ b/app/javascript/packages/verify-flow/steps/personal-key/personal-key-step.spec.tsx @@ -3,6 +3,7 @@ import * as analytics from '@18f/identity-analytics'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import PersonalKeyStep from './personal-key-step'; +import { AddressVerificationMethodContextProvider } from '../../context/address-verification-method-context'; describe('PersonalKeyStep', () => { const sandbox = sinon.createSandbox(); @@ -49,11 +50,31 @@ describe('PersonalKeyStep', () => { expect(analytics.trackEvent).to.have.been.calledWith('IdV: print personal key'); }); - it('renders success alert', () => { - const { getByRole } = render(); + context('with gpo as address verification method', () => { + it('renders success alert', () => { + const { getByRole } = render( + + + , + ); - const status = getByRole('status'); + const status = getByRole('status'); - expect(status.textContent).to.equal('idv.messages.confirm'); + expect(status.textContent).to.equal('idv.messages.mail_sent'); + }); + }); + + context('with phone as address verification method', () => { + it('renders success alert', () => { + const { getByRole } = render( + + + , + ); + + const status = getByRole('status'); + + expect(status.textContent).to.equal('idv.messages.confirm'); + }); }); }); diff --git a/app/javascript/packages/verify-flow/steps/personal-key/personal-key-step.tsx b/app/javascript/packages/verify-flow/steps/personal-key/personal-key-step.tsx index 45dceb738cc..f8827dacff1 100644 --- a/app/javascript/packages/verify-flow/steps/personal-key/personal-key-step.tsx +++ b/app/javascript/packages/verify-flow/steps/personal-key/personal-key-step.tsx @@ -1,3 +1,4 @@ +import { useContext } from 'react'; import { Alert, PageHeading } from '@18f/identity-components'; import { ClipboardButton } from '@18f/identity-clipboard-button'; import { PrintButton } from '@18f/identity-print-button'; @@ -8,17 +9,21 @@ import type { FormStepComponentProps } from '@18f/identity-form-steps'; import { getAssetPath } from '@18f/identity-assets'; import { trackEvent } from '@18f/identity-analytics'; import type { VerifyFlowValues } from '../../verify-flow'; +import AddressVerificationMethodContext from '../../context/address-verification-method-context'; import DownloadButton from './download-button'; interface PersonalKeyStepProps extends FormStepComponentProps {} function PersonalKeyStep({ value }: PersonalKeyStepProps) { const personalKey = value.personalKey!; + const { addressVerificationMethod } = useContext(AddressVerificationMethodContext); return ( <> - {t('idv.messages.confirm')} + {addressVerificationMethod === 'phone' + ? t('idv.messages.confirm') + : t('idv.messages.mail_sent')} {t('headings.personal_key')}

{t('instructions.personal_key.info')}

diff --git a/app/javascript/packages/verify-flow/verify-flow-step-indicator.spec.tsx b/app/javascript/packages/verify-flow/verify-flow-step-indicator.spec.tsx index 7c2086a2257..136b1e6cf31 100644 --- a/app/javascript/packages/verify-flow/verify-flow-step-indicator.spec.tsx +++ b/app/javascript/packages/verify-flow/verify-flow-step-indicator.spec.tsx @@ -1,5 +1,6 @@ import { render } from '@testing-library/react'; import { StepStatus } from '@18f/identity-step-indicator'; +import { AddressVerificationMethodContextProvider } from './context/address-verification-method-context'; import VerifyFlowStepIndicator, { getStepStatus } from './verify-flow-step-indicator'; describe('getStepStatus', () => { @@ -32,4 +33,17 @@ describe('VerifyFlowStepIndicator', () => { const previous = getByText('step_indicator.flows.idv.verify_phone_or_address'); expect(previous.closest('.step-indicator__step--complete')).to.exist(); }); + + context('with gpo as address verification method', () => { + it('renders address verification as pending', () => { + const { getByText } = render( + + + , + ); + + const previous = getByText('step_indicator.flows.idv.verify_phone_or_address'); + expect(previous.closest('.step-indicator__step--pending')).to.exist(); + }); + }); }); diff --git a/app/javascript/packages/verify-flow/verify-flow-step-indicator.tsx b/app/javascript/packages/verify-flow/verify-flow-step-indicator.tsx index 16bf41696ff..1932a531a60 100644 --- a/app/javascript/packages/verify-flow/verify-flow-step-indicator.tsx +++ b/app/javascript/packages/verify-flow/verify-flow-step-indicator.tsx @@ -1,11 +1,8 @@ +import { useContext } from 'react'; import { StepIndicator, StepIndicatorStep, StepStatus } from '@18f/identity-step-indicator'; import { t } from '@18f/identity-i18n'; - -// i18n-tasks-use t('step_indicator.flows.idv.getting_started') -// i18n-tasks-use t('step_indicator.flows.idv.verify_id') -// i18n-tasks-use t('step_indicator.flows.idv.verify_info') -// i18n-tasks-use t('step_indicator.flows.idv.verify_phone_or_address') -// i18n-tasks-use t('step_indicator.flows.idv.secure_account') +import AddressVerificationMethodContext from './context/address-verification-method-context'; +import type { AddressVerificationMethod } from './context/address-verification-method-context'; type VerifyFlowStepIndicatorStep = | 'getting_started' @@ -62,8 +59,38 @@ export function getStepStatus(index, currentStepIndex): StepStatus { return StepStatus.INCOMPLETE; } +/** + * Given contextual details of the current flow path, returns explicit statuses which should be used + * at particular steps. + * + * @param details Flow details + * + * @return Step status overrides. + */ +function getStatusOverrides({ + addressVerificationMethod, +}: { + addressVerificationMethod: AddressVerificationMethod; +}) { + const statuses: Partial> = {}; + + if (addressVerificationMethod === 'gpo') { + statuses.verify_phone_or_address = StepStatus.PENDING; + } + + return statuses; +} + function VerifyFlowStepIndicator({ currentStep }: VerifyFlowStepIndicatorProps) { const currentStepIndex = STEP_INDICATOR_STEPS.indexOf(FLOW_STEP_STEP_MAPPING[currentStep]); + const { addressVerificationMethod } = useContext(AddressVerificationMethodContext); + const statusOverrides = getStatusOverrides({ addressVerificationMethod }); + + // i18n-tasks-use t('step_indicator.flows.idv.getting_started') + // i18n-tasks-use t('step_indicator.flows.idv.verify_id') + // i18n-tasks-use t('step_indicator.flows.idv.verify_info') + // i18n-tasks-use t('step_indicator.flows.idv.verify_phone_or_address') + // i18n-tasks-use t('step_indicator.flows.idv.secure_account') return ( @@ -71,7 +98,7 @@ function VerifyFlowStepIndicator({ currentStep }: VerifyFlowStepIndicatorProps) ))} diff --git a/app/javascript/packages/verify-flow/verify-flow.spec.tsx b/app/javascript/packages/verify-flow/verify-flow.spec.tsx index 80f48d8b575..8fc5a5f9ce1 100644 --- a/app/javascript/packages/verify-flow/verify-flow.spec.tsx +++ b/app/javascript/packages/verify-flow/verify-flow.spec.tsx @@ -1,3 +1,4 @@ +import sinon from 'sinon'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as analytics from '@18f/identity-analytics'; @@ -12,7 +13,8 @@ describe('VerifyFlow', () => { beforeEach(() => { sandbox.spy(analytics, 'trackEvent'); sandbox.stub(window, 'fetch').resolves({ - json: () => Promise.resolve({ personal_key: personalKey }), + json: () => + Promise.resolve({ personal_key: personalKey, completion_url: 'http://example.com' }), } as Response); document.body.innerHTML = ``; }); @@ -50,7 +52,9 @@ describe('VerifyFlow', () => { await userEvent.type(getByLabelText('forms.personal_key.confirmation_label'), personalKey); await userEvent.keyboard('{Enter}'); - expect(onComplete).to.have.been.called(); + expect(onComplete).to.have.been.calledWith( + sinon.match({ completionURL: 'http://example.com' }), + ); expect(sessionStorage.getItem('completedStep')).to.be.null(); }); diff --git a/app/javascript/packages/verify-flow/verify-flow.tsx b/app/javascript/packages/verify-flow/verify-flow.tsx index 654dadf78bb..99aaf291ddc 100644 --- a/app/javascript/packages/verify-flow/verify-flow.tsx +++ b/app/javascript/packages/verify-flow/verify-flow.tsx @@ -8,6 +8,10 @@ import VerifyFlowStepIndicator from './verify-flow-step-indicator'; import { useSyncedSecretValues } from './context/secrets-context'; import FlowContext from './context/flow-context'; import useInitialStepValidation from './hooks/use-initial-step-validation'; +import { + AddressVerificationMethod, + AddressVerificationMethodContextProvider, +} from './context/address-verification-method-context'; export interface VerifyFlowValues { userBundleToken?: string; @@ -37,6 +41,8 @@ export interface VerifyFlowValues { password?: string; dob?: string; + + completionURL?: string; } interface VerifyFlowProps { @@ -65,10 +71,15 @@ interface VerifyFlowProps { */ cancelURL?: string; + /** + * Initial value for address verification method. + */ + initialAddressVerificationMethod?: AddressVerificationMethod; + /** * Callback invoked after completing the form. */ - onComplete: () => void; + onComplete: (values: VerifyFlowValues) => void; } /** @@ -98,6 +109,7 @@ function VerifyFlow({ basePath, startOverURL = '', cancelURL = '', + initialAddressVerificationMethod, onComplete, }: VerifyFlowProps) { let steps = STEPS; @@ -118,26 +130,28 @@ function VerifyFlow({ setCompletedStep(stepName); } - function onFormComplete() { + function onFormComplete(values: VerifyFlowValues) { setCompletedStep(null); - onComplete(); + onComplete(values); } return ( - - + + + + ); } diff --git a/app/javascript/packs/verify-flow.tsx b/app/javascript/packs/verify-flow.tsx index 2b4094472bc..a61ff269b64 100644 --- a/app/javascript/packs/verify-flow.tsx +++ b/app/javascript/packs/verify-flow.tsx @@ -1,5 +1,9 @@ import { render } from 'react-dom'; -import { VerifyFlow, SecretsContextProvider } from '@18f/identity-verify-flow'; +import { + VerifyFlow, + SecretsContextProvider, + AddressVerificationMethod, +} from '@18f/identity-verify-flow'; import SecretSessionStorage, { s2ab } from '@18f/identity-secret-session-storage'; import type { SecretValues, VerifyFlowValues } from '@18f/identity-verify-flow'; @@ -29,20 +33,20 @@ interface AppRootValues { */ cancelUrl: string; - /** - * URL to which user should be redirected after completing the form. - */ - completionUrl: string; - /** * Base64-encoded encryption key for secret session store. */ storeKey: string; +} - /** - * Signed JWT containing user data. - */ - userBundleToken: string; +interface UserBundleMetadata { + address_verification_mechanism: AddressVerificationMethod; +} + +interface UserBundle { + pii: Record; + + metadata: UserBundleMetadata; } interface AppRootElement extends HTMLElement { @@ -56,7 +60,6 @@ const { basePath, startOverUrl: startOverURL, cancelUrl: cancelURL, - completionUrl: completionURL, storeKey: storeKeyBase64, } = appRoot.dataset; const storeKey = s2ab(atob(storeKeyBase64)); @@ -78,20 +81,16 @@ const storage = new SecretSessionStorage('verify'); ]); storage.key = cryptoKey; await storage.load(); - if (initialValues.userBundleToken) { - await storage.setItem('userBundleToken', initialValues.userBundleToken); - } - - const userBundleToken = storage.getItem('userBundleToken'); - if (userBundleToken) { - const jwtData = JSON.parse(atob(userBundleToken.split('.')[1])); - const pii = Object.fromEntries(mapKeys(jwtData.pii, camelCase)); - Object.assign(initialValues, pii); - } + const userBundleToken = initialValues.userBundleToken as string; + await storage.setItem('userBundleToken', userBundleToken); + const { pii, metadata } = JSON.parse(atob(userBundleToken.split('.')[1])) as UserBundle; + Object.assign(initialValues, Object.fromEntries(mapKeys(pii, camelCase))); - function onComplete() { + function onComplete({ completionURL }: VerifyFlowValues) { storage.clear(); - window.location.href = completionURL; + if (completionURL) { + window.location.href = completionURL; + } } render( @@ -103,6 +102,7 @@ const storage = new SecretSessionStorage('verify'); cancelURL={cancelURL} basePath={basePath} onComplete={onComplete} + initialAddressVerificationMethod={metadata.address_verification_mechanism} /> , appRoot, diff --git a/app/jobs/remove_old_throttles_job.rb b/app/jobs/remove_old_throttles_job.rb deleted file mode 100644 index 56be2bb65d9..00000000000 --- a/app/jobs/remove_old_throttles_job.rb +++ /dev/null @@ -1,44 +0,0 @@ -class RemoveOldThrottlesJob < ApplicationJob - queue_as :low - - WINDOW = 30.days.freeze - - include GoodJob::ActiveJobExtensions::Concurrency - - good_job_control_concurrency_with( - total_limit: 1, - key: -> do - rounded = TimeService.round_time(time: arguments.first, interval: 1.hour) - "remove-old-throttles-#{rounded.to_i}" - end, - ) - - discard_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError - - def perform(now, limit: 500, total_limit: 50_000) - max_window = Throttle::THROTTLE_CONFIG.map { |_, config| config[:attempt_window] }.max - total_removed = 0 - - loop do - removed_count = DatabaseThrottle. - where('updated_at < ?', now - (WINDOW + max_window.minutes)). - or(DatabaseThrottle.where(updated_at: nil)). - limit(limit). - delete_all - - total_removed += removed_count - - Rails.logger.info( - { - name: 'remove_old_throttles', - removed_count: removed_count, - total_removed: total_removed, - total_limit: total_limit, - }.to_json, - ) - - break if removed_count.zero? - break if total_removed >= total_limit - end - end -end diff --git a/app/models/concerns/deprecated_user_attributes.rb b/app/models/concerns/deprecated_user_attributes.rb index 147404d4f0d..3bb60134096 100644 --- a/app/models/concerns/deprecated_user_attributes.rb +++ b/app/models/concerns/deprecated_user_attributes.rb @@ -2,7 +2,7 @@ module DeprecatedUserAttributes extend ActiveSupport::Concern DEPRECATED_ATTRIBUTES = %i[ - email_fingerprint encrypted_email email confirmed_at confirmation_token confirmation_sent_at + email_fingerprint encrypted_email email confirmed_at ].freeze def []=(attribute, value) diff --git a/app/models/user.rb b/app/models/user.rb index 8eec4e70361..87f13543af6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,5 @@ class User < ApplicationRecord - self.ignored_columns = %w[totp_timestamp confirmation_token confirmation_sent_at] + self.ignored_columns = %w[totp_timestamp] include NonNullUuid include ::NewRelic::Agent::MethodTracer @@ -41,7 +41,6 @@ class User < ApplicationRecord has_many :auth_app_configurations, dependent: :destroy, inverse_of: :user has_many :backup_code_configurations, dependent: :destroy has_many :document_capture_sessions, dependent: :destroy - has_many :database_throttles, dependent: :destroy has_one :registration_log, dependent: :destroy has_one :proofing_component, dependent: :destroy has_many :service_providers, diff --git a/app/presenters/cancellation_presenter.rb b/app/presenters/cancellation_presenter.rb index 209e04529f6..8172dacb267 100644 --- a/app/presenters/cancellation_presenter.rb +++ b/app/presenters/cancellation_presenter.rb @@ -9,7 +9,7 @@ def initialize(referer:, url_options:) end def go_back_path - referer_path || two_factor_options_path + referer_path || authentication_methods_setup_path end def url_options diff --git a/app/presenters/navigation_presenter.rb b/app/presenters/navigation_presenter.rb index c4f5c2f4918..80539314dd9 100644 --- a/app/presenters/navigation_presenter.rb +++ b/app/presenters/navigation_presenter.rb @@ -17,7 +17,7 @@ def navigation_items NavItem.new(I18n.t('account.navigation.add_email'), add_email_path), NavItem.new(I18n.t('account.navigation.edit_password'), manage_password_path), NavItem.new(I18n.t('account.navigation.delete_account'), account_delete_path), - user.encrypted_recovery_code_digest.present? ? NavItem.new( + user.encrypted_recovery_code_digest.present? && user.active_profile ? NavItem.new( I18n.t('account.navigation.reset_personal_key'), create_new_personal_key_path ) : nil, ].compact diff --git a/app/services/analytics.rb b/app/services/analytics.rb index 68dbc048ee1..773cca74999 100644 --- a/app/services/analytics.rb +++ b/app/services/analytics.rb @@ -137,16 +137,6 @@ def session_started_at # rubocop:disable Layout/LineLength DOC_AUTH = 'Doc Auth' # visited or submitted is appended - MULTI_FACTOR_AUTH_MAX_ATTEMPTS = 'Multi-Factor Authentication: max attempts reached' - MULTI_FACTOR_AUTH_OPTION_LIST = 'Multi-Factor Authentication: option list' - MULTI_FACTOR_AUTH_OPTION_LIST_VISIT = 'Multi-Factor Authentication: option list visited' - MULTI_FACTOR_AUTH_PHONE_SETUP = 'Multi-Factor Authentication: phone setup' - MULTI_FACTOR_AUTH_MAX_SENDS = 'Multi-Factor Authentication: max otp sends reached' - MULTI_FACTOR_AUTH_SETUP = 'Multi-Factor Authentication Setup' - OPENID_CONNECT_BEARER_TOKEN = 'OpenID Connect: bearer token authentication' - OPENID_CONNECT_REQUEST_AUTHORIZATION = 'OpenID Connect: authorization request' - OPENID_CONNECT_TOKEN = 'OpenID Connect: token' - OTP_DELIVERY_SELECTION = 'OTP: Delivery Selection' PASSWORD_RESET_TOKEN = 'Password Reset: Token Submitted' PASSWORD_RESET_VISIT = 'Password Reset: Email Form Visited' PENDING_ACCOUNT_RESET_CANCELLED = 'Pending account reset cancelled' diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index a9c78062875..48a3cef1536 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -777,6 +777,7 @@ def idv_phone_confirmation_otp_resent( # @param [String] country_code country code of phone number # @param [String] area_code area code of phone number # @param [Boolean] rate_limit_exceeded whether or not the rate limit was exceeded by this attempt + # @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164 # @param [Hash] telephony_response response from Telephony gem # The user requested an OTP to confirm their phone during the IDV phone step def idv_phone_confirmation_otp_sent( @@ -786,6 +787,7 @@ def idv_phone_confirmation_otp_sent( country_code:, area_code:, rate_limit_exceeded:, + phone_fingerprint:, telephony_response:, **extra ) @@ -797,6 +799,7 @@ def idv_phone_confirmation_otp_sent( country_code: country_code, area_code: area_code, rate_limit_exceeded: rate_limit_exceeded, + phone_fingerprint: phone_fingerprint, telephony_response: telephony_response, **extra, ) @@ -1137,6 +1140,156 @@ def multi_factor_auth_enter_webauthn_visit( ) end + # Max multi factor auth attempts met + def multi_factor_auth_max_attempts + track_event('Multi-Factor Authentication: max attempts reached') + end + + # Multi factor selected from auth options list + # @param [Boolean] success + # @param [Hash] errors + # @param [String] selection + def multi_factor_auth_option_list(success:, errors:, selection:, **extra) + track_event( + 'Multi-Factor Authentication: option list', + success: success, + errors: errors, + selection: selection, + **extra, + ) + end + + # User visited the list of multi-factor options to use + def multi_factor_auth_option_list_visit + track_event('Multi-Factor Authentication: option list visited') + end + + # Multi factor auth phone setup + # @param [Boolean] success + # @param [Hash] errors + # @param [String] otp_delivery_preference + # @param [String] area_code + # @param [String] carrier + # @param [String] country_code + # @param [String] phone_type + # @param [Hash] types + # @param [Hash] pii_like_keypaths + def multi_factor_auth_phone_setup(success:, + errors:, + otp_delivery_preference:, + area_code:, + carrier:, + country_code:, + phone_type:, + types:, + **extra) + + track_event( + 'Multi-Factor Authentication: phone setup', + success: success, + errors: errors, + otp_delivery_preference: otp_delivery_preference, + area_code: area_code, + carrier: carrier, + country_code: country_code, + phone_type: phone_type, + types: types, + **extra, + ) + end + + # Max multi factor max otp sends reached + def multi_factor_auth_max_sends + track_event('Multi-Factor Authentication: max otp sends reached') + end + + # Tracks when a user sets up a multi factor auth method + # @param [String] multi_factor_auth_method + def multi_factor_auth_setup(multi_factor_auth_method:, **extra) + track_event( + 'Multi-Factor Authentication Setup', + multi_factor_auth_method: multi_factor_auth_method, + **extra, + ) + end + + # Tracks when an openid connect bearer token authentication request is made + # @param [Boolean] success + # @param [Hash] errors + def openid_connect_bearer_token(success:, errors:, **extra) + track_event( + 'OpenID Connect: bearer token authentication', + success: success, + errors: errors, + **extra, + ) + end + + # Tracks when openid authorization request is made + # @param [String] client_id + # @param [String] scope + # @param [Array] acr_values + # @param [Boolean] unauthorized_scope + # @param [Boolean] user_fully_authenticated + def openid_connect_request_authorization( + client_id:, + scope:, + acr_values:, + unauthorized_scope:, + user_fully_authenticated:, + **extra + ) + track_event( + 'OpenID Connect: authorization request', + client_id: client_id, + scope: scope, + acr_values: acr_values, + unauthorized_scope: unauthorized_scope, + user_fully_authenticated: user_fully_authenticated, + **extra, + ) + end + + # Tracks when an openid connect token request is made + # @param [String] client_id + # @param [String] user_id + def openid_connect_token(client_id:, user_id:, **extra) + track_event( + 'OpenID Connect: token', + client_id: client_id, + user_id: user_id, + **extra, + ) + end + + # Tracks when user makes an otp delivery selection + # @param [String] otp_delivery_preference (sms or voice) + # @param [Boolean] resend + # @param [String] country_code + # @param [String] area_code + # @param ["authentication","reauthentication","confirmation"] context user session context + # @param [Hash] pii_like_keypaths + def otp_delivery_selection( + otp_delivery_preference:, + resend:, + country_code:, + area_code:, + context:, + pii_like_keypaths:, + **extra + ) + track_event( + 'OTP: Delivery Selection', + otp_delivery_preference: otp_delivery_preference, + resend: resend, + country_code: country_code, + area_code: area_code, + context: context, + pii_like_keypaths: pii_like_keypaths, + **extra, + ) + end + # @param [Boolean] success # @param [Hash] errors # The user updated their password @@ -1391,6 +1544,43 @@ def saml_auth_request( ) end + # @param [String] area_code + # @param [String] country_code + # @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164 + # @param [String] context the context of the OTP, either "authentication" for confirmed phones + # or "confirmation" for unconfirmed + # @param ["sms","voice"] otp_delivery_preference the channel used to send the message + # @param [Boolean] resend + # @param [Hash] telephony_response + # @param [Boolean] success + # A phone one-time password send was attempted + def telephony_otp_sent( + area_code:, + country_code:, + phone_fingerprint:, + context:, + otp_delivery_preference:, + resend:, + telephony_response:, + success:, + **extra + ) + track_event( + 'Telephony: OTP sent', + { + area_code: area_code, + country_code: country_code, + phone_fingerprint: phone_fingerprint, + context: context, + otp_delivery_preference: otp_delivery_preference, + resend: resend, + telephony_response: telephony_response, + success: success, + **extra, + }, + ) + end + # @param [Boolean] success # @param [Hash] errors # Tracks when the the user has selected and submitted additional MFA methods on user registration diff --git a/app/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window.rb b/app/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window.rb index eb16b4651fe..e7c090d1507 100644 --- a/app/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window.rb +++ b/app/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window.rb @@ -128,6 +128,7 @@ def partial_month_subqueries(issuer:, partial_months:) sp_return_logs.requested_at::date BETWEEN %{range_start} AND %{range_end} AND sp_return_logs.returned_at IS NOT NULL AND sp_return_logs.issuer = %{issuer} + AND sp_return_logs.billable = true GROUP BY sp_return_logs.user_id , sp_return_logs.ial diff --git a/app/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa.rb b/app/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa.rb index 498ead6e2f3..df505c918e8 100644 --- a/app/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa.rb +++ b/app/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa.rb @@ -119,6 +119,7 @@ def partial_month_subqueries(issuers:, partial_months:) sp_return_logs.requested_at::date BETWEEN %{range_start} AND %{range_end} AND sp_return_logs.returned_at IS NOT NULL AND sp_return_logs.issuer IN %{issuers} + AND sp_return_logs.billable = true GROUP BY sp_return_logs.user_id , sp_return_logs.ial diff --git a/app/services/doc_auth/mock/result_response_builder.rb b/app/services/doc_auth/mock/result_response_builder.rb index 04d128b0964..091baae7f1f 100644 --- a/app/services/doc_auth/mock/result_response_builder.rb +++ b/app/services/doc_auth/mock/result_response_builder.rb @@ -100,7 +100,7 @@ def pii_from_doc raw_pii = parsed_data_from_uploaded_file['document'] raw_pii&.symbolize_keys || {} else - Idp::Constants::DEFAULT_MOCK_PII_FROM_DOC + Idp::Constants::MOCK_IDV_APPLICANT end end diff --git a/app/services/idv/flows/in_person_flow.rb b/app/services/idv/flows/in_person_flow.rb index 8f236ae02db..69f24e4fc7b 100644 --- a/app/services/idv/flows/in_person_flow.rb +++ b/app/services/idv/flows/in_person_flow.rb @@ -8,20 +8,15 @@ class InPersonFlow < Flow::BaseFlow state_id: Idv::Steps::Ipp::StateIdStep, # info from state id ssn: Idv::Steps::Ipp::SsnStep, # enter SSN verify: Idv::Steps::Ipp::VerifyStep, # verify entered info - # WILLFIX: add the failure branch for verify step - # WILLFIX: add the verify by mail flow - phone: Idv::Steps::Ipp::PhoneStep, # phone finder - # WILLFIX: add the failure branch for phone step - # WILLFIX: re-use existing password confirm step - password_confirm: Idv::Steps::Ipp::PasswordConfirmStep, - # WILLFIX: re-use existing personal key step - personal_key: Idv::Steps::Ipp::PersonalKeyStep, - barcode: Idv::Steps::Ipp::BarcodeStep, }.freeze ACTIONS = { }.freeze + # WILLFIX: (LG-6308) move this to the barcode page when we finish setting up IPP step + # indicators + # i18n-tasks-use t('step_indicator.flows.idv.go_to_the_post_office') + STEP_INDICATOR_STEPS = [ { name: :find_a_post_office }, { name: :verify_info }, @@ -32,11 +27,20 @@ class InPersonFlow < Flow::BaseFlow def initialize(controller, session, name) @idv_session = self.class.session_idv(session) - super(controller, STEPS, {}, session[name]) + super(controller, STEPS, ACTIONS, session[name]) end def self.session_idv(session) session[:idv] ||= { params: {}, step_attempts: { phone: 0 } } + # WILLFIX: remove this line when we begin collecting user data + session[:idv][:applicant] ||= Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN + + # WILLFIX: (LG-6349) remove this block when we implement the verify page + session[:idv]['profile_confirmation'] = true + session[:idv]['vendor_phone_confirmation'] = false + session[:idv]['user_phone_confirmation'] = false + session[:idv]['address_verification_mechanism'] = 'phone' + session[:idv]['resolution_successful'] = 'phone' end end end diff --git a/app/services/idv/send_phone_confirmation_otp.rb b/app/services/idv/send_phone_confirmation_otp.rb index abc9e8bc235..4a9c79cdaa6 100644 --- a/app/services/idv/send_phone_confirmation_otp.rb +++ b/app/services/idv/send_phone_confirmation_otp.rb @@ -80,6 +80,7 @@ def extra_analytics_attributes otp_delivery_preference: delivery_method, country_code: parsed_phone.country, area_code: parsed_phone.area_code, + phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164), rate_limit_exceeded: rate_limit_exceeded?, telephony_response: @telephony_response, } diff --git a/app/services/idv/steps/ipp/barcode_step.rb b/app/services/idv/steps/ipp/barcode_step.rb deleted file mode 100644 index ff6cea38fae..00000000000 --- a/app/services/idv/steps/ipp/barcode_step.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Idv - module Steps - module Ipp - class BarcodeStep < DocAuthBaseStep - # i18n-tasks-use t('step_indicator.flows.idv.go_to_the_post_office') - STEP_INDICATOR_STEP = :go_to_the_post_office - def call; end - end - end - end -end diff --git a/app/services/idv/steps/ipp/password_confirm_step.rb b/app/services/idv/steps/ipp/password_confirm_step.rb deleted file mode 100644 index 8fd40b46ffe..00000000000 --- a/app/services/idv/steps/ipp/password_confirm_step.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Idv - module Steps - module Ipp - class PasswordConfirmStep < DocAuthBaseStep - STEP_INDICATOR_STEP = :secure_account - def call; end - end - end - end -end diff --git a/app/services/idv/steps/ipp/personal_key_step.rb b/app/services/idv/steps/ipp/personal_key_step.rb deleted file mode 100644 index 0ca4189392b..00000000000 --- a/app/services/idv/steps/ipp/personal_key_step.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Idv - module Steps - module Ipp - class PersonalKeyStep < DocAuthBaseStep - STEP_INDICATOR_STEP = :secure_account - def call; end - end - end - end -end diff --git a/app/services/idv/steps/ipp/phone_step.rb b/app/services/idv/steps/ipp/phone_step.rb deleted file mode 100644 index bbfa77b81c0..00000000000 --- a/app/services/idv/steps/ipp/phone_step.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Idv - module Steps - module Ipp - class PhoneStep < DocAuthBaseStep - STEP_INDICATOR_STEP = :verify_phone_or_address - def call; end - end - end - end -end diff --git a/app/services/idv/steps/ipp/verify_step.rb b/app/services/idv/steps/ipp/verify_step.rb index 6b49fc9db2b..025308b2b51 100644 --- a/app/services/idv/steps/ipp/verify_step.rb +++ b/app/services/idv/steps/ipp/verify_step.rb @@ -3,7 +3,11 @@ module Steps module Ipp class VerifyStep < DocAuthBaseStep STEP_INDICATOR_STEP = :verify_info - def call; end + def call + # send the user to the phone page where they'll continue the remainder of + # the idv flow + redirect_to idv_phone_url + end end end end diff --git a/app/services/idv/steps/verify_base_step.rb b/app/services/idv/steps/verify_base_step.rb index 1fa97d28854..ee66c7118bd 100644 --- a/app/services/idv/steps/verify_base_step.rb +++ b/app/services/idv/steps/verify_base_step.rb @@ -1,11 +1,6 @@ module Idv module Steps class VerifyBaseStep < DocAuthBaseStep - AAMVA_SUPPORTED_JURISDICTIONS = %w[ - AR AZ CO CT DC DE FL GA IA ID IL IN KS KY MA MD ME MI MO MS MT NC ND NE - NJ NM OH OR PA RI SC SD TN TX VA VT WA WI WY - ].to_set.freeze - private def summarize_result_and_throttle_failures(summary_result) @@ -78,7 +73,9 @@ def should_use_aamva?(pii_from_doc) end def aamva_state?(pii_from_doc) - AAMVA_SUPPORTED_JURISDICTIONS.include?(pii_from_doc['state_id_jurisdiction']) + IdentityConfig.store.aamva_supported_jurisdictions.include?( + pii_from_doc['state_id_jurisdiction'], + ) end def aamva_disallowed_for_service_provider? diff --git a/app/services/proofing/mock/state_id_mock_client.rb b/app/services/proofing/mock/state_id_mock_client.rb index 51c09db834a..e87f1ed7f2b 100644 --- a/app/services/proofing/mock/state_id_mock_client.rb +++ b/app/services/proofing/mock/state_id_mock_client.rb @@ -42,7 +42,7 @@ class StateIdMockClient < Proofing::Base private def state_not_supported?(state_id_jurisdiction) - !Idv::Steps::VerifyBaseStep::AAMVA_SUPPORTED_JURISDICTIONS.include? state_id_jurisdiction + !IdentityConfig.store.aamva_supported_jurisdictions.include? state_id_jurisdiction end def invalid_state_id_number?(state_id_number) diff --git a/app/services/throttle.rb b/app/services/throttle.rb index 7d35261b7af..df0e89c150e 100644 --- a/app/services/throttle.rb +++ b/app/services/throttle.rb @@ -75,19 +75,15 @@ def initialize(throttle_type:, user: nil, target: nil) end def attempts - if IdentityConfig.store.redis_throttle_enabled - redis_attempts.to_i - else - postgres_throttle.attempts - end + return @redis_attempts.to_i if defined?(@redis_attempts) + + fetch_state! + + @redis_attempts.to_i end def throttled? - if IdentityConfig.store.redis_throttle_enabled - !expired? && maxed? - else - postgres_throttle.throttled? - end + !expired? && maxed? end def throttled_else_increment? @@ -100,46 +96,22 @@ def throttled_else_increment? end def attempted_at - if IdentityConfig.store.redis_throttle_enabled - redis_attempted_at - else - postgres_throttle.attempted_at - end + return @redis_attempted_at if defined?(@redis_attempted_at) + + fetch_state! + + @redis_attempted_at end def expires_at - if IdentityConfig.store.redis_throttle_enabled - return Time.zone.now if redis_attempted_at.blank? - redis_attempted_at + Throttle.attempt_window_in_minutes(throttle_type).minutes - else - postgres_throttle.expires_at - end + return Time.zone.now if attempted_at.blank? + attempted_at + Throttle.attempt_window_in_minutes(throttle_type).minutes end def remaining_count return 0 if throttled? - if IdentityConfig.store.redis_throttle_enabled - Throttle.max_attempts(throttle_type) - attempts - else - postgres_throttle.remaining_count - end - end - - def redis_attempts - return @redis_attempts if defined?(@redis_attempts) - - fetch_state! - - @redis_attempts - end - - def redis_attempted_at - return @redis_attempted_at if defined?(@redis_attempted_at) - - fetch_state! - - @redis_attempted_at + Throttle.max_attempts(throttle_type) - attempts end def expired? @@ -147,7 +119,7 @@ def expired? end def maxed? - redis_attempts && redis_attempts >= Throttle.max_attempts(throttle_type) + attempts && attempts >= Throttle.max_attempts(throttle_type) end def increment! @@ -165,8 +137,6 @@ def increment! @redis_attempts = value.to_i @redis_attempted_at = Time.zone.now - postgres_throttle.increment - attempts end @@ -205,8 +175,6 @@ def reset! client.del(key) end - postgres_throttle.reset - @redis_attempts = 0 @redis_attempted_at = nil end @@ -225,11 +193,6 @@ def increment_to_throttled! @redis_attempts = value.to_i @redis_attempted_at = Time.zone.now - postgres_throttle.update( - attempts: Throttle.max_attempts(throttle_type), - attempted_at: Time.zone.now, - ) - attempts end @@ -241,16 +204,6 @@ def key end end - def postgres_throttle - return @postgres_throttle if @postgres_throttle - - @postgres_throttle ||= DatabaseThrottle.for( - throttle_type: throttle_type, - user: @user, - target: @target, - ) - end - def self.attempt_window_in_minutes(throttle_type) THROTTLE_CONFIG.dig(throttle_type, :attempt_window) end diff --git a/app/views/idv/doc_auth/welcome.html.erb b/app/views/idv/doc_auth/welcome.html.erb index f93e9253cc8..dd10278335d 100644 --- a/app/views/idv/doc_auth/welcome.html.erb +++ b/app/views/idv/doc_auth/welcome.html.erb @@ -89,7 +89,7 @@ <%= render 'shared/cancel', link: idv_cancel_path(step: 'welcome') %> <% else %>
- <%= link_to(t('two_factor_authentication.choose_another_option'), two_factor_options_path) %> + <%= link_to(t('two_factor_authentication.choose_another_option'), authentication_methods_setup_path) %>
<% end %> <% end %> diff --git a/app/views/idv/in_person/password_confirm.html.erb b/app/views/idv/in_person/password_confirm.html.erb deleted file mode 100644 index 6be83dfccef..00000000000 --- a/app/views/idv/in_person/password_confirm.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<% title t('titles.doc_auth.verify') %> - -

- <%= t('in_person_proofing.headings.password_confirm', app_name: APP_NAME) %> -

- -<%= validated_form_for :doc_auth, - url: url_for, - method: 'put', - html: { autocomplete: 'off', class: 'margin-y-5' } do |f| %> - <%= f.button :button, - t('doc_auth.buttons.continue'), - type: :submit, - class: 'usa-button--big usa-button--wide' %> -<% end %> diff --git a/app/views/idv/in_person/personal_key.html.erb b/app/views/idv/in_person/personal_key.html.erb deleted file mode 100644 index 79313e2f127..00000000000 --- a/app/views/idv/in_person/personal_key.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<% title t('titles.doc_auth.verify') %> - -

- <%= t('in_person_proofing.headings.personal_key') %> -

- -<%= validated_form_for :doc_auth, - url: url_for, - method: 'put', - html: { autocomplete: 'off', class: 'margin-y-5' } do |f| %> - <%= f.button :button, - t('doc_auth.buttons.continue'), - type: :submit, - class: 'usa-button--big usa-button--wide' %> -<% end %> diff --git a/app/views/idv/in_person/phone.html.erb b/app/views/idv/in_person/phone.html.erb deleted file mode 100644 index 51100ad4d1d..00000000000 --- a/app/views/idv/in_person/phone.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<% title t('titles.doc_auth.verify') %> - -

- <%= t('in_person_proofing.headings.phone') %> -

- -<%= validated_form_for :doc_auth, - url: url_for, - method: 'put', - html: { autocomplete: 'off', class: 'margin-y-5' } do |f| %> - <%= f.button :button, - t('doc_auth.buttons.continue'), - type: :submit, - class: 'usa-button--big usa-button--wide' %> -<% end %> diff --git a/app/views/shared/_cancel_or_back_to_options.html.erb b/app/views/shared/_cancel_or_back_to_options.html.erb index 24e1043e479..0e8e4222890 100644 --- a/app/views/shared/_cancel_or_back_to_options.html.erb +++ b/app/views/shared/_cancel_or_back_to_options.html.erb @@ -2,6 +2,6 @@ <% if MfaPolicy.new(current_user).two_factor_enabled? %> <%= link_to t('links.cancel'), account_path %> <% else %> - <%= link_to t('two_factor_authentication.choose_another_option'), two_factor_options_path %> + <%= link_to t('two_factor_authentication.choose_another_option'), authentication_methods_setup_path %> <% end %> <% end %> diff --git a/app/views/sign_up/cancellations/new.html.erb b/app/views/sign_up/cancellations/new.html.erb index 386c62d24b5..0d8d6f75094 100644 --- a/app/views/sign_up/cancellations/new.html.erb +++ b/app/views/sign_up/cancellations/new.html.erb @@ -14,19 +14,11 @@
  • <%= t('users.delete.bullet_4', app_name: APP_NAME) %>
  • - <% if IdentityConfig.store.new_sign_up_cancellation_url_enabled %> - <% c.action_button( - action: ->(**tag_options, &block) do - button_to(sign_up_destroy_path, method: :delete, **tag_options, &block) - end, - ) { t('forms.buttons.cancel') } %> - <% else %> - <% c.action_button( - action: ->(**tag_options, &block) do - button_to(destroy_user_path, method: :delete, **tag_options, &block) - end, - ) { t('forms.buttons.cancel') } %> - <% end %> + <% c.action_button( + action: ->(**tag_options, &block) do + button_to(sign_up_destroy_path, method: :delete, **tag_options, &block) + end, + ) { t('forms.buttons.cancel') } %> <% c.action_button( action: ->(**tag_options, &block) do diff --git a/app/views/users/mfa_selection/index.html.erb b/app/views/users/mfa_selection/index.html.erb index b51cda2bb9b..8da0ce458b6 100644 --- a/app/views/users/mfa_selection/index.html.erb +++ b/app/views/users/mfa_selection/index.html.erb @@ -2,6 +2,8 @@ <%= render PageHeadingComponent.new.with_content(t('two_factor_authentication.two_factor_choice')) %> +

    <%= @presenter.intro %>

    + <%= validated_form_for @two_factor_options_form, html: { autocomplete: 'off' }, method: :patch, @@ -21,6 +23,6 @@ <%= f.button :submit, t('forms.buttons.continue'), class: 'usa-button--big usa-button--wide margin-bottom-1' %> <% end %> -<%= render 'shared/cancel', link: destroy_user_session_path %> +<%= render 'shared/cancel', link: @after_setup_path %> <%= javascript_packs_tag_once('webauthn-unhide') %> diff --git a/app/views/users/phone_setup/index.html.erb b/app/views/users/phone_setup/index.html.erb index 26db6c61c61..cb8091f16fd 100644 --- a/app/views/users/phone_setup/index.html.erb +++ b/app/views/users/phone_setup/index.html.erb @@ -40,5 +40,5 @@ <% end %> <%= render PageFooterComponent.new do %> - <%= link_to t('two_factor_authentication.choose_another_option'), two_factor_options_path %> + <%= link_to t('two_factor_authentication.choose_another_option'), authentication_methods_setup_path %> <% end %> diff --git a/app/views/users/piv_cac_authentication_setup/error.html.erb b/app/views/users/piv_cac_authentication_setup/error.html.erb index 1854d359069..4556e2298f7 100644 --- a/app/views/users/piv_cac_authentication_setup/error.html.erb +++ b/app/views/users/piv_cac_authentication_setup/error.html.erb @@ -12,6 +12,6 @@ <% if MfaPolicy.new(current_user).two_factor_enabled? %> <%= link_to t('links.cancel'), account_path %> <% else %> - <%= link_to t('two_factor_authentication.choose_another_option'), two_factor_options_path %> + <%= link_to t('two_factor_authentication.choose_another_option'), authentication_methods_setup_path %> <% end %> <% end %> diff --git a/app/views/users/two_factor_authentication_setup/index.html.erb b/app/views/users/two_factor_authentication_setup/index.html.erb index 09efca23017..a77bcfc27f0 100644 --- a/app/views/users/two_factor_authentication_setup/index.html.erb +++ b/app/views/users/two_factor_authentication_setup/index.html.erb @@ -17,7 +17,7 @@ <%= validated_form_for @two_factor_options_form, html: { autocomplete: 'off' }, method: :patch, - url: two_factor_options_path do |f| %> + url: authentication_methods_setup_path do |f| %>
    <%= @presenter.intro %> diff --git a/config/application.yml.default b/config/application.yml.default index ff751701e70..cfb51a0311e 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -18,6 +18,7 @@ aamva_auth_request_timeout: 5.0 aamva_auth_url: 'https://example.org:12345/auth/url' aamva_cert_enabled: true aamva_sp_banlist_issuers: '[]' +aamva_supported_jurisdictions: '["AR","AZ","CO","CT","DC","DE","FL","GA","HI","IA","ID","IL","IN","KS","KY","MA","MD","ME","MI","MO","MS","MT","NC","ND","NE","NJ","NM","OH","OR","PA","RI","SC","SD","TN","TX","VA","VT","WA","WI","WY"]' aamva_verification_request_timeout: 5.0 aamva_verification_url: https://example.org:12345/verification/url all_redirect_uris_cache_duration_minutes: 2 @@ -380,7 +381,6 @@ production: piv_cac_verify_token_secret: platform_authentication_enabled: false session_encryptor_alert_enabled: true - session_encryptor_v2_enabled: false recurring_jobs_disabled_names: "[]" redis_irs_attempt_api_url: redis://redis.login.gov.internal:6379/2 redis_throttle_url: redis://redis.login.gov.internal:6379/1 diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index 21fe8f3dac3..c51dbe76413 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -189,12 +189,6 @@ cron: cron_24h, args: -> { [Time.zone.yesterday] }, }, - # Removes old rows from the Throttles table - remove_old_throttles: { - class: 'RemoveOldThrottlesJob', - cron: cron_1h, - args: -> { [Time.zone.now] }, - }, # Sync opted out phone numbers from AWS phone_number_opt_out_sync_job: { class: 'PhoneNumberOptOutSyncJob', diff --git a/config/locales/in_person_proofing/en.yml b/config/locales/in_person_proofing/en.yml index a48e1ce774f..22a6358ced2 100644 --- a/config/locales/in_person_proofing/en.yml +++ b/config/locales/in_person_proofing/en.yml @@ -5,9 +5,6 @@ en: address: Enter your current address barcode: You’re ready to verify your identity in person location: Select a location to verify your ID - password_confirm: Re-enter your %{app_name} password to protect your data - personal_key: Save your personal key - phone: Enter a phone number with your name on the plan ssn: Enter your Social Security number state_id: Enter the information on your ID verify: Please verify your information diff --git a/config/locales/in_person_proofing/es.yml b/config/locales/in_person_proofing/es.yml index 63b35b9d2ec..7e64684e067 100644 --- a/config/locales/in_person_proofing/es.yml +++ b/config/locales/in_person_proofing/es.yml @@ -5,9 +5,6 @@ es: address: Ingrese su dirección actual barcode: Estás listo para verificar tu identidad en persona location: Seleccione una ubicación para verificar su identificación - password_confirm: Vuelve a ingresar tu contraseña de %{app_name} para encriptar tus datos - personal_key: Guarda tu clave personal - phone: Ingresa un número de teléfono para ayudar a verificar tu identidad ssn: Ingresa tu número del seguro social state_id: Introduce los datos de su documento de identidad verify: Por favor verifique su información diff --git a/config/locales/in_person_proofing/fr.yml b/config/locales/in_person_proofing/fr.yml index c5894dd20b9..2ca75fc2863 100644 --- a/config/locales/in_person_proofing/fr.yml +++ b/config/locales/in_person_proofing/fr.yml @@ -5,9 +5,6 @@ fr: address: Entrez votre adresse actuelle barcode: Vous êtes prêt à vérifier votre identité en personne location: Sélectionnez un emplacement pour vérifier votre identité - password_confirm: Entrez à nouveau votre mot de passe %{app_name} pour crypter vos données - personal_key: Enregistrez votre clé personnelle - phone: Entrez un numéro de téléphone pour vous aider à vérifier votre identité ssn: Entrez votre numéro de sécurité sociale state_id: Entrez les informations sur votre pièce d’identité verify: Veuillez vérifier vos informations diff --git a/config/routes.rb b/config/routes.rb index 28e67cec754..69a71bffffe 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -222,9 +222,10 @@ post '/account/personal_key' => 'accounts/personal_keys#create' get '/otp/send' => 'users/two_factor_authentication#send_code' + get '/authentication_methods_setup' => 'users/two_factor_authentication_setup#index' patch '/authentication_methods_setup' => 'users/two_factor_authentication_setup#create' - get '/two_factor_options' => 'users/two_factor_authentication_setup#index' + get '/two_factor_options', to: redirect('/authentication_methods_setup') patch '/two_factor_options' => 'users/two_factor_authentication_setup#create' get '/second_mfa_setup' => 'users/mfa_selection#index' patch '/second_mfa_setup' => 'users/mfa_selection#update' @@ -272,9 +273,6 @@ match '/sign_out' => 'sign_out#destroy', via: %i[get post delete] - # Deprecated - delete '/users' => 'users#destroy', as: :destroy_user - get '/restricted' => 'banned_user#show', as: :banned_user scope '/verify', as: 'idv' do diff --git a/db/primary_migrate/20220602005747_drop_deprecated_user_columns.rb b/db/primary_migrate/20220602005747_drop_deprecated_user_columns.rb new file mode 100644 index 00000000000..fdd37e7bb70 --- /dev/null +++ b/db/primary_migrate/20220602005747_drop_deprecated_user_columns.rb @@ -0,0 +1,19 @@ +class DropDeprecatedUserColumns < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def up + remove_index :users, column: [:confirmation_token], name: 'index_users_on_confirmation_token', algorithm: :concurrently + + safety_assured do + remove_column :users, :confirmation_token + remove_column :users, :confirmation_sent_at + end + end + + def down + add_column :users, :confirmation_token, :text + add_column :users, :confirmation_sent_at, :datetime + + add_index :users, ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index b163470de3c..2110bd7575e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_05_17_103312) do +ActiveRecord::Schema.define(version: 2022_06_02_005747) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" @@ -581,9 +581,7 @@ t.datetime "remember_created_at" t.datetime "created_at" t.datetime "updated_at" - t.string "confirmation_token", limit: 255 t.datetime "confirmed_at" - t.datetime "confirmation_sent_at" t.integer "second_factor_attempts_count", default: 0 t.string "uuid", limit: 255, null: false t.datetime "second_factor_locked_at" @@ -602,7 +600,6 @@ t.string "email_language", limit: 10 t.datetime "accepted_terms_at" t.datetime "encrypted_recovery_code_digest_generated_at" - t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["uuid"], name: "index_users_on_uuid", unique: true end diff --git a/lib/identity_config.rb b/lib/identity_config.rb index fd015990fcc..d78bbb321cc 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -73,6 +73,7 @@ def self.build_store(config_map) config.add(:aamva_private_key, type: :string) config.add(:aamva_public_key, type: :string) config.add(:aamva_sp_banlist_issuers, type: :json) + config.add(:aamva_supported_jurisdictions, type: :json) config.add(:aamva_verification_request_timeout, type: :float) config.add(:aamva_verification_url) config.add(:all_redirect_uris_cache_duration_minutes, type: :integer) diff --git a/lib/idp/constants.rb b/lib/idp/constants.rb index 6860cdcde23..417c9f481a1 100644 --- a/lib/idp/constants.rb +++ b/lib/idp/constants.rb @@ -11,7 +11,7 @@ module Constants AAL2 = 2 AAL3 = 3 - DEFAULT_MOCK_PII_FROM_DOC = { + MOCK_IDV_APPLICANT = { first_name: 'FAKEY', middle_name: nil, last_name: 'MCFAKERSON', @@ -27,5 +27,9 @@ module Constants state_id_expiration: '2099-12-31', phone: nil, }.freeze + + MOCK_IDV_APPLICANT_WITH_SSN = MOCK_IDV_APPLICANT.merge(ssn: '900-66-1234').freeze + + MOCK_IDV_APPLICANT_WITH_PHONE = MOCK_IDV_APPLICANT_WITH_SSN.merge(phone: '12025551212').freeze end end diff --git a/lib/session_encryptor.rb b/lib/session_encryptor.rb index 8805a9b4cef..3d7ebd68501 100644 --- a/lib/session_encryptor.rb +++ b/lib/session_encryptor.rb @@ -23,7 +23,7 @@ class SensitiveValueError < StandardError; end ['email'], ] - SENSITIVE_DEFAULT_FIELDS = Idp::Constants::DEFAULT_MOCK_PII_FROM_DOC.slice( + SENSITIVE_DEFAULT_FIELDS = Idp::Constants::MOCK_IDV_APPLICANT.slice( :last_name, :address1, :city, diff --git a/package.json b/package.json index 3959967d335..593e30ce77c 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "fast-glob": "^3.2.7", "focus-trap": "^6.7.1", "foundation-emails": "^2.3.1", - "identity-style-guide": "^6.4.2", + "identity-style-guide": "^6.5.0", "intl-tel-input": "^17.0.8", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/spec/controllers/account_reset/request_controller_spec.rb b/spec/controllers/account_reset/request_controller_spec.rb index 4c5b54290f0..cf4cd94dd66 100644 --- a/spec/controllers/account_reset/request_controller_spec.rb +++ b/spec/controllers/account_reset/request_controller_spec.rb @@ -21,7 +21,7 @@ stub_sign_in_before_2fa get :show - expect(response).to redirect_to two_factor_options_url + expect(response).to redirect_to authentication_methods_setup_url end it 'logs the visit to analytics' do @@ -100,7 +100,7 @@ stub_sign_in_before_2fa post :create - expect(response).to redirect_to two_factor_options_url + expect(response).to redirect_to authentication_methods_setup_url end end end diff --git a/spec/controllers/api/verify/base_controller_spec.rb b/spec/controllers/api/verify/base_controller_spec.rb new file mode 100644 index 00000000000..6ec69686c12 --- /dev/null +++ b/spec/controllers/api/verify/base_controller_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +describe Api::Verify::BaseController do + describe '#show' do + subject(:response) { get :show } + + context 'without required_step defined' do + controller Api::Verify::BaseController do + def show; end + end + + before { routes.draw { get '/' => 'api/verify/base#show' } } + + it 'raises an exception' do + expect { response }.to raise_error(NotImplementedError) + end + end + + context 'with required_step defined' do + controller Api::Verify::BaseController do + self.required_step = 'example' + + def show + render json: {} + end + end + + before { routes.draw { get '/' => 'api/verify/base#show' } } + + it 'renders as not found (404)' do + expect(response.status).to eq(404) + end + + context 'with step enabled' do + before do + allow(IdentityConfig.store).to receive(:idv_api_enabled_steps).and_return(['example']) + end + + it 'renders as unauthorized (401)' do + expect(response.status).to eq(401) + end + + context 'with authenticated user' do + before { stub_sign_in } + + it 'renders as ok (200)' do + expect(response.status).to eq(200) + end + end + end + end + end +end diff --git a/spec/controllers/api/verify/password_confirm_controller_spec.rb b/spec/controllers/api/verify/password_confirm_controller_spec.rb index ab00ade7b6d..ed1db9f33b3 100644 --- a/spec/controllers/api/verify/password_confirm_controller_spec.rb +++ b/spec/controllers/api/verify/password_confirm_controller_spec.rb @@ -10,37 +10,19 @@ def stub_idv_session let(:password) { 'iambatman' } let(:user) { create(:user, :signed_up, password: password) } - let(:applicant) do - { first_name: 'Bruce', - last_name: 'Wayne', - address1: '123 Mansion St', - address2: 'Ste 456', - city: 'Gotham City', - state: 'NY', - zipcode: '10015' } - end - - let(:pii) do - { first_name: 'Bruce', - last_name: 'Wayne', - ssn: '900-90-1234' } - end + let(:applicant) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE } let(:profile) { subject.idv_session.profile } let(:key) { OpenSSL::PKey::RSA.new(Base64.strict_decode64(IdentityConfig.store.idv_private_key)) } - let(:jwt) { JWT.encode({ pii: pii, metadata: {} }, key, 'RS256', sub: user.uuid) } + let(:jwt_metadata) { { vendor_phone_confirmation: true, user_phone_confirmation: true } } + let(:jwt) { JWT.encode({ pii: applicant, metadata: jwt_metadata }, key, 'RS256', sub: user.uuid) } before do - allow(IdentityConfig.store).to receive(:idv_api_enabled_steps).and_return(['personal_key']) + allow(IdentityConfig.store).to receive(:idv_api_enabled_steps).and_return(['password_confirm']) end - describe 'before_actions' do - it 'includes before_actions from Api::BaseController' do - expect(subject).to have_actions( - :before, - :confirm_two_factor_authenticated_for_api, - ) - end + it 'extends behavior of base api class' do + expect(subject).to be_kind_of Api::Verify::BaseController end describe '#create' do @@ -58,9 +40,13 @@ def stub_idv_session stub_idv_session end - it 'creates a profile and returns a key' do + it 'creates a profile and returns a key and completion url' do post :create, params: { password: 'iambatman', user_bundle_token: jwt } - expect(JSON.parse(response.body)['personal_key']).not_to be_nil + parsed_body = JSON.parse(response.body) + expect(parsed_body).to include( + 'personal_key' => kind_of(String), + 'completion_url' => account_url, + ) expect(response.status).to eq 200 end @@ -71,6 +57,28 @@ def stub_idv_session expect(response_json['error']['password']).to eq([I18n.t('idv.errors.incorrect_password')]) expect(response.status).to eq 400 end + + context 'with associated sp session' do + before do + session[:sp] = { issuer: create(:service_provider).issuer } + end + + it 'creates a profile and returns completion url' do + post :create, params: { password: 'iambatman', user_bundle_token: jwt } + + expect(JSON.parse(response.body)['completion_url']).to eq(sign_up_completed_url) + end + end + + context 'with pending profile' do + let(:jwt_metadata) { { vendor_phone_confirmation: false, user_phone_confirmation: false } } + + it 'creates a profile and returns completion url' do + post :create, params: { password: 'iambatman', user_bundle_token: jwt } + + expect(JSON.parse(response.body)['completion_url']).to eq(idv_come_back_later_url) + end + end end context 'when the idv api is not enabled' do diff --git a/spec/controllers/api/verify/password_reset_controller_spec.rb b/spec/controllers/api/verify/password_reset_controller_spec.rb index 814f412e3c3..d12d4fab7e2 100644 --- a/spec/controllers/api/verify/password_reset_controller_spec.rb +++ b/spec/controllers/api/verify/password_reset_controller_spec.rb @@ -6,13 +6,13 @@ let(:sp_session) { { request_id: request_id } } before do - allow(IdentityConfig.store).to receive(:idv_api_enabled_steps).and_return(['personal_key']) + allow(IdentityConfig.store).to receive(:idv_api_enabled_steps).and_return(['password_confirm']) allow(controller).to receive(:sp_session).and_return(sp_session) stub_sign_in(user) end it 'extends behavior of base api class' do - expect(subject).to be_kind_of Api::BaseController + expect(subject).to be_kind_of Api::Verify::BaseController end describe '#create' do diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index bef613d7310..70d74a5c73a 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -184,7 +184,7 @@ def index get :index - expect(response).to redirect_to two_factor_options_url + expect(response).to redirect_to authentication_methods_setup_url end end diff --git a/spec/controllers/concerns/render_condition_concern_spec.rb b/spec/controllers/concerns/render_condition_concern_spec.rb index 985b6ff25d2..13ca98d7384 100644 --- a/spec/controllers/concerns/render_condition_concern_spec.rb +++ b/spec/controllers/concerns/render_condition_concern_spec.rb @@ -9,6 +9,7 @@ check_or_render_not_found -> { FeatureManagement.all_feature? } check_or_render_not_found -> { FeatureManagement.show_feature? }, only: [:show] + check_or_render_not_found -> { instance_condition? } def index render plain: '' @@ -17,6 +18,12 @@ def index def show render plain: '' end + + private + + def instance_condition? + true + end end before do diff --git a/spec/controllers/idv/resend_otp_controller_spec.rb b/spec/controllers/idv/resend_otp_controller_spec.rb index ea6eada9999..a12f37ea8d2 100644 --- a/spec/controllers/idv/resend_otp_controller_spec.rb +++ b/spec/controllers/idv/resend_otp_controller_spec.rb @@ -54,6 +54,7 @@ expected_result = { success: true, + phone_fingerprint: Pii::Fingerprinter.fingerprint(Phonelib.parse(phone).e164), errors: {}, otp_delivery_preference: :sms, country_code: 'US', diff --git a/spec/controllers/idv/review_controller_spec.rb b/spec/controllers/idv/review_controller_spec.rb index bf9eb9d893e..efc94e05bd9 100644 --- a/spec/controllers/idv/review_controller_spec.rb +++ b/spec/controllers/idv/review_controller_spec.rb @@ -365,7 +365,7 @@ def show context 'with idv app personal key step enabled' do before do allow(IdentityConfig.store).to receive(:idv_api_enabled_steps). - and_return(['personal_key']) + and_return(['password_confirm', 'personal_key', 'personal_key_confirm']) end it 'redirects to idv app personal key path' do @@ -389,19 +389,6 @@ def show expect(profile).to_not be_active end - - context 'with idv api personal key step enabled' do - before do - allow(IdentityConfig.store).to receive(:idv_api_enabled_steps). - and_return(['personal_key']) - end - - it 'redirects to personal key path' do - put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } - - expect(response).to redirect_to idv_personal_key_path - end - end end end end diff --git a/spec/controllers/idv_controller_spec.rb b/spec/controllers/idv_controller_spec.rb index 7ad3e74026a..a2d35c8d9b3 100644 --- a/spec/controllers/idv_controller_spec.rb +++ b/spec/controllers/idv_controller_spec.rb @@ -38,7 +38,7 @@ end it 'redirects to account recovery if user has a password reset profile' do - profile = create(:profile, deactivation_reason: :password_reset) + profile = create(:profile, :password_reset) stub_sign_in(profile.user) allow(subject.reactivate_account_session).to receive(:started?).and_return(true) diff --git a/spec/controllers/openid_connect/authorization_controller_spec.rb b/spec/controllers/openid_connect/authorization_controller_spec.rb index 8c20d909644..b9b68abdb67 100644 --- a/spec/controllers/openid_connect/authorization_controller_spec.rb +++ b/spec/controllers/openid_connect/authorization_controller_spec.rb @@ -50,7 +50,7 @@ it 'tracks IAL1 authentication event' do stub_analytics expect(@analytics).to receive(:track_event). - with(Analytics::OPENID_CONNECT_REQUEST_AUTHORIZATION, + with('OpenID Connect: authorization request', success: true, client_id: client_id, errors: {}, @@ -109,7 +109,7 @@ it 'tracks IAL2 authentication event' do stub_analytics expect(@analytics).to receive(:track_event). - with(Analytics::OPENID_CONNECT_REQUEST_AUTHORIZATION, + with('OpenID Connect: authorization request', success: true, client_id: client_id, errors: {}, @@ -208,7 +208,7 @@ it 'tracks IAL2 authentication event' do stub_analytics expect(@analytics).to receive(:track_event). - with(Analytics::OPENID_CONNECT_REQUEST_AUTHORIZATION, + with('OpenID Connect: authorization request', success: true, client_id: client_id, errors: {}, @@ -249,7 +249,7 @@ it 'tracks IAL1 authentication event' do stub_analytics expect(@analytics).to receive(:track_event). - with(Analytics::OPENID_CONNECT_REQUEST_AUTHORIZATION, + with('OpenID Connect: authorization request', success: true, client_id: client_id, errors: {}, @@ -291,7 +291,7 @@ it 'tracks IAL1 authentication event' do stub_analytics expect(@analytics).to receive(:track_event). - with(Analytics::OPENID_CONNECT_REQUEST_AUTHORIZATION, + with('OpenID Connect: authorization request', success: true, client_id: client_id, errors: {}, @@ -368,7 +368,7 @@ it 'tracks the event with errors' do stub_analytics expect(@analytics).to receive(:track_event). - with(Analytics::OPENID_CONNECT_REQUEST_AUTHORIZATION, + with('OpenID Connect: authorization request', success: false, client_id: client_id, unauthorized_scope: true, @@ -396,7 +396,7 @@ it 'tracks the event with errors' do stub_analytics expect(@analytics).to receive(:track_event). - with(Analytics::OPENID_CONNECT_REQUEST_AUTHORIZATION, + with('OpenID Connect: authorization request', success: false, client_id: nil, unauthorized_scope: true, diff --git a/spec/controllers/openid_connect/token_controller_spec.rb b/spec/controllers/openid_connect/token_controller_spec.rb index c0354bb3969..03be4f8b7a8 100644 --- a/spec/controllers/openid_connect/token_controller_spec.rb +++ b/spec/controllers/openid_connect/token_controller_spec.rb @@ -53,9 +53,11 @@ it 'tracks a successful event in analytics' do stub_analytics expect(@analytics).to receive(:track_event). - with(Analytics::OPENID_CONNECT_TOKEN, success: true, client_id: client_id, - user_id: user.uuid, errors: {}) - + with('OpenID Connect: token', + success: true, + client_id: client_id, + user_id: user.uuid, + errors: {}) action end end @@ -75,7 +77,7 @@ it 'tracks an unsuccessful event in analytics' do stub_analytics expect(@analytics).to receive(:track_event). - with(Analytics::OPENID_CONNECT_TOKEN, + with('OpenID Connect: token', success: false, client_id: client_id, user_id: user.uuid, diff --git a/spec/controllers/openid_connect/user_info_controller_spec.rb b/spec/controllers/openid_connect/user_info_controller_spec.rb index 4dcacdc4dda..179154e8a03 100644 --- a/spec/controllers/openid_connect/user_info_controller_spec.rb +++ b/spec/controllers/openid_connect/user_info_controller_spec.rb @@ -20,7 +20,7 @@ it 'tracks analytics' do stub_analytics expect(@analytics).to receive(:track_event). - with(Analytics::OPENID_CONNECT_BEARER_TOKEN, + with('OpenID Connect: bearer token authentication', success: false, errors: hash_including(:access_token), error_details: hash_including(:access_token)) @@ -42,7 +42,7 @@ it 'tracks analytics' do stub_analytics expect(@analytics).to receive(:track_event). - with(Analytics::OPENID_CONNECT_BEARER_TOKEN, + with('OpenID Connect: bearer token authentication', success: false, errors: hash_including(:access_token), error_details: hash_including(:access_token)) @@ -63,7 +63,7 @@ it 'tracks analytics' do stub_analytics expect(@analytics).to receive(:track_event). - with(Analytics::OPENID_CONNECT_BEARER_TOKEN, + with('OpenID Connect: bearer token authentication', success: false, errors: hash_including(:access_token), error_details: hash_including(:access_token)) @@ -96,7 +96,7 @@ it 'tracks analytics' do stub_analytics expect(@analytics).to receive(:track_event). - with(Analytics::OPENID_CONNECT_BEARER_TOKEN, success: true, errors: {}) + with('OpenID Connect: bearer token authentication', success: true, errors: {}) action end diff --git a/spec/controllers/reactivate_account_controller_spec.rb b/spec/controllers/reactivate_account_controller_spec.rb index 2cbfe9662b4..b441eed9ea5 100644 --- a/spec/controllers/reactivate_account_controller_spec.rb +++ b/spec/controllers/reactivate_account_controller_spec.rb @@ -14,7 +14,7 @@ describe '#index' do context 'with a password reset profile' do - let(:profiles) { [create(:profile, deactivation_reason: :password_reset)] } + let(:profiles) { [create(:profile, :password_reset)] } it 'renders the index template' do get :index @@ -34,7 +34,7 @@ end describe '#update' do - let(:profiles) { [create(:profile, deactivation_reason: :password_reset)] } + let(:profiles) { [create(:profile, :password_reset)] } it 'redirects user to idv_url' do put :update diff --git a/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb index b5654fd5cb1..89a9d282d98 100644 --- a/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb @@ -121,7 +121,8 @@ expect(@analytics).to receive(:track_mfa_submit_event). with(properties) - expect(@analytics).to receive(:track_event).with(Analytics::MULTI_FACTOR_AUTH_MAX_ATTEMPTS) + expect(@analytics).to receive(:track_event). + with('Multi-Factor Authentication: max attempts reached') expect(PushNotification::HttpPush).to receive(:deliver). with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user)) diff --git a/spec/controllers/two_factor_authentication/options_controller_spec.rb b/spec/controllers/two_factor_authentication/options_controller_spec.rb index b0a7c9813ff..52770aedf5b 100644 --- a/spec/controllers/two_factor_authentication/options_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/options_controller_spec.rb @@ -15,7 +15,7 @@ stub_analytics expect(@analytics).to receive(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_OPTION_LIST_VISIT) + with('Multi-Factor Authentication: option list visited') get :index end @@ -87,7 +87,7 @@ } expect(@analytics).to receive(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_OPTION_LIST, result) + with('Multi-Factor Authentication: option list', result) post :create, params: { two_factor_options_form: { selection: 'sms' } } end diff --git a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb index 96a457c3670..5a7d02e4e9f 100644 --- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb @@ -147,7 +147,8 @@ expect(@analytics).to receive(:track_mfa_submit_event). with(properties) - expect(@analytics).to receive(:track_event).with(Analytics::MULTI_FACTOR_AUTH_MAX_ATTEMPTS) + expect(@analytics).to receive(:track_event). + with('Multi-Factor Authentication: max attempts reached') expect(PushNotification::HttpPush).to receive(:deliver). with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user)) @@ -317,7 +318,7 @@ } expect(@analytics).to receive(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_SETUP, properties) + with('Multi-Factor Authentication Setup', properties) controller.user_session[:phone_id] = phone_id post( :create, @@ -380,7 +381,7 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_SETUP, properties) + with('Multi-Factor Authentication Setup', properties) end end @@ -424,7 +425,7 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_SETUP, properties) + with('Multi-Factor Authentication Setup', properties) expect(subject).to have_received(:create_user_event).with(:phone_confirmed) expect(subject).to have_received(:create_user_event).exactly(:once) 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 93fd44df368..fb0fc9f30b8 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 @@ -35,7 +35,7 @@ get :show expect(response.status).to eq(302) - expect(response.location).to eq(two_factor_options_url) + expect(response.location).to eq(authentication_methods_setup_url) end end @@ -90,7 +90,7 @@ post :create, params: { personal_key_form: { personal_key: raw_key } } expect(response.status).to eq(302) - expect(response.location).to eq(two_factor_options_url) + expect(response.location).to eq(authentication_methods_setup_url) end context 'when the personal key field is empty' do @@ -155,7 +155,8 @@ expect(@analytics).to receive(:track_mfa_submit_event). with(properties) - expect(@analytics).to receive(:track_event).with(Analytics::MULTI_FACTOR_AUTH_MAX_ATTEMPTS) + expect(@analytics).to receive(:track_event). + with('Multi-Factor Authentication: max attempts reached') expect(PushNotification::HttpPush).to receive(:deliver). with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user)) diff --git a/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb index 0ba1f392468..2f69c558cbc 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 @@ -194,7 +194,8 @@ expect(@analytics).to receive(:track_mfa_submit_event). with(submit_attributes) - expect(@analytics).to receive(:track_event).with(Analytics::MULTI_FACTOR_AUTH_MAX_ATTEMPTS) + expect(@analytics).to receive(:track_event). + with('Multi-Factor Authentication: max attempts reached') expect(PushNotification::HttpPush).to receive(:deliver). with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user)) diff --git a/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb index 99447075c8b..de3a9337678 100644 --- a/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb @@ -89,7 +89,8 @@ expect(@analytics).to receive(:track_mfa_submit_event). with(attributes) - expect(@analytics).to receive(:track_event).with(Analytics::MULTI_FACTOR_AUTH_MAX_ATTEMPTS) + expect(@analytics).to receive(:track_event). + with('Multi-Factor Authentication: max attempts reached') expect(PushNotification::HttpPush).to receive(:deliver). with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user)) diff --git a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb index 81c5c6a25df..07c9744dd05 100644 --- a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb @@ -58,6 +58,7 @@ client_data_json: verification_client_data_json, signature: signature, credential_id: credential_id, + platform: '', } end before do diff --git a/spec/controllers/users/phone_setup_controller_spec.rb b/spec/controllers/users/phone_setup_controller_spec.rb index 3bf91ad5b96..ee6a58a9282 100644 --- a/spec/controllers/users/phone_setup_controller_spec.rb +++ b/spec/controllers/users/phone_setup_controller_spec.rb @@ -63,7 +63,7 @@ } expect(@analytics).to receive(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_PHONE_SETUP, result) + with('Multi-Factor Authentication: phone setup', result) patch :create, params: { new_phone_form: { @@ -95,7 +95,7 @@ } expect(@analytics).to receive(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_PHONE_SETUP, result) + with('Multi-Factor Authentication: phone setup', result) patch( :create, @@ -135,7 +135,7 @@ } expect(@analytics).to receive(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_PHONE_SETUP, result) + with('Multi-Factor Authentication: phone setup', result) patch( :create, @@ -174,7 +174,7 @@ } expect(@analytics).to receive(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_PHONE_SETUP, result) + with('Multi-Factor Authentication: phone setup', result) patch( :create, diff --git a/spec/controllers/users/totp_setup_controller_spec.rb b/spec/controllers/users/totp_setup_controller_spec.rb index e53cdfc8f06..d1eccba9ff1 100644 --- a/spec/controllers/users/totp_setup_controller_spec.rb +++ b/spec/controllers/users/totp_setup_controller_spec.rb @@ -103,7 +103,7 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) + with('Multi-Factor Authentication Setup', result) end end @@ -132,7 +132,7 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) + with('Multi-Factor Authentication Setup', result) end end @@ -162,7 +162,7 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) + with('Multi-Factor Authentication Setup', result) end end @@ -193,7 +193,7 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) + with('Multi-Factor Authentication Setup', result) end end end @@ -222,7 +222,7 @@ auth_app_configuration_id: nil, } expect(@analytics).to have_received(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) + with('Multi-Factor Authentication Setup', result) end end @@ -254,7 +254,7 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) + with('Multi-Factor Authentication Setup', result) end end @@ -273,7 +273,7 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) + with('Multi-Factor Authentication Setup', result) end end end @@ -301,7 +301,7 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) + with('Multi-Factor Authentication Setup', result) end end end diff --git a/spec/controllers/users/two_factor_authentication_controller_spec.rb b/spec/controllers/users/two_factor_authentication_controller_spec.rb index 84e9a9b01c4..2ecc2aa6931 100644 --- a/spec/controllers/users/two_factor_authentication_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_controller_spec.rb @@ -236,7 +236,7 @@ def index stub_sign_in_before_2fa(build(:user)) get :show - expect(response).to redirect_to two_factor_options_url + expect(response).to redirect_to authentication_methods_setup_url end end @@ -265,7 +265,7 @@ def index it 'redirects to MFA setup if no PIV/CAC is enabled' do get :show - expect(response).to redirect_to(two_factor_options_url) + expect(response).to redirect_to(authentication_methods_setup_url) end end end @@ -304,7 +304,7 @@ def index success: true, errors: {}, otp_delivery_preference: 'sms', - resend: nil, + resend: 'true', context: 'authentication', country_code: 'US', area_code: '202', @@ -313,12 +313,15 @@ def index expect(@analytics).to receive(:track_event). ordered. - with(Analytics::OTP_DELIVERY_SELECTION, analytics_hash) + with('OTP: Delivery Selection', analytics_hash) expect(@analytics).to receive(:track_event). ordered. - with(Analytics::TELEPHONY_OTP_SENT, hash_including(success: true)) + with('Telephony: OTP sent', hash_including( + resend: 'true', success: true, otp_delivery_preference: 'sms', + )) - get :send_code, params: { otp_delivery_selection_form: { otp_delivery_preference: 'sms' } } + get :send_code, params: { otp_delivery_selection_form: + { otp_delivery_preference: 'sms', resend: 'true' } } end it 'calls OtpRateLimiter#exceeded_otp_send_limit? and #increment' do @@ -337,14 +340,18 @@ def index it 'marks the user as locked out after too many attempts' do expect(@user.second_factor_locked_at).to be_nil - (IdentityConfig.store.otp_delivery_blocklist_maxretry + 1).times do - get :send_code, params: { - otp_delivery_selection_form: { otp_delivery_preference: 'sms', - otp_make_default_number: nil }, - } - end + freeze_time do + (IdentityConfig.store.otp_delivery_blocklist_maxretry + 1).times do + get :send_code, params: { + otp_delivery_selection_form: { + otp_delivery_preference: 'sms', + otp_make_default_number: nil, + }, + } + end - expect(@user.reload.second_factor_locked_at.to_f).to be_within(0.1).of(Time.zone.now.to_f) + expect(@user.reload.second_factor_locked_at).to eq Time.zone.now + end end context 'when the phone has been marked as opted out in the DB' do @@ -430,10 +437,13 @@ def index expect(@analytics).to receive(:track_event). ordered. - with(Analytics::OTP_DELIVERY_SELECTION, analytics_hash) + with('OTP: Delivery Selection', analytics_hash) expect(@analytics).to receive(:track_event). ordered. - with(Analytics::TELEPHONY_OTP_SENT, hash_including(success: true)) + with('Telephony: OTP sent', hash_including( + success: true, + otp_delivery_preference: 'voice', + )) get :send_code, params: { otp_delivery_selection_form: { otp_delivery_preference: 'voice', @@ -506,7 +516,7 @@ def index timeout: timeout, ), ) - expect(response).to redirect_to two_factor_options_url + expect(response).to redirect_to authentication_methods_setup_url end end diff --git a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb index 139e3700ee6..d0c9ea0d814 100644 --- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb @@ -113,7 +113,7 @@ end it 'the redirect to the form page with an anchor' do - expect(response).to redirect_to(two_factor_options_path(anchor: 'select_phone')) + expect(response).to redirect_to(authentication_methods_setup_path(anchor: 'select_phone')) end it 'contains a flash message' do expect(flash[:phone_error]).to eq( diff --git a/spec/controllers/users/verify_password_controller_spec.rb b/spec/controllers/users/verify_password_controller_spec.rb index 5ae08df32ed..45da44b7327 100644 --- a/spec/controllers/users/verify_password_controller_spec.rb +++ b/spec/controllers/users/verify_password_controller_spec.rb @@ -21,7 +21,7 @@ end context 'with password reset profile' do - let(:profiles) { [create(:profile, deactivation_reason: :password_reset)] } + let(:profiles) { [create(:profile, :password_reset)] } let(:response_ok) { FormResponse.new(success: true, errors: {}, extra: { personal_key: key }) } let(:response_bad) { FormResponse.new(success: false, errors: {}) } let(:key) { 'key' } diff --git a/spec/controllers/users/verify_personal_key_controller_spec.rb b/spec/controllers/users/verify_personal_key_controller_spec.rb index c797d2f0fc7..c109a9d4dd2 100644 --- a/spec/controllers/users/verify_personal_key_controller_spec.rb +++ b/spec/controllers/users/verify_personal_key_controller_spec.rb @@ -22,7 +22,7 @@ end context 'with password reset profile' do - let!(:profiles) { [create(:profile, user: user, deactivation_reason: :password_reset)] } + let!(:profiles) { [create(:profile, :password_reset, user: user)] } it 'renders the `new` template' do get :new @@ -46,7 +46,7 @@ end context 'with throttle reached' do - let!(:profiles) { [create(:profile, user: user, deactivation_reason: :password_reset)] } + let!(:profiles) { [create(:profile, :password_reset, user: user)] } before do Throttle.new(throttle_type: :verify_personal_key, user: user).increment_to_throttled! @@ -74,8 +74,9 @@ [ create( :profile, - user: user, deactivation_reason: :password_reset, - pii: { ssn: '123456789' } + :password_reset, + user: user, + pii: { ssn: '123456789' }, ), ] } diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb index 62832d4f03c..e9f66f3a72a 100644 --- a/spec/controllers/users/webauthn_setup_controller_spec.rb +++ b/spec/controllers/users/webauthn_setup_controller_spec.rb @@ -77,7 +77,7 @@ multi_factor_auth_method: 'webauthn', } expect(@analytics).to receive(:track_event). - with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) + with('Multi-Factor Authentication Setup', result) patch :confirm, params: params end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb deleted file mode 100644 index 9c8edf6cfa6..00000000000 --- a/spec/controllers/users_controller_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'rails_helper' - -describe UsersController do - describe '#destroy' do - it 'redirects and displays the flash message if no user is present' do - delete :destroy - - expect(response).to redirect_to(root_url) - expect(flash.now[:success]).to eq t('sign_up.cancel.success') - end - - it 'destroys the current user and redirects to sign in page, with a helpful flash message' do - sign_in_as_user - subject.session[:user_confirmation_token] = '1' - - expect { delete :destroy }.to change(User, :count).by(-1) - expect(response).to redirect_to(root_url) - expect(flash.now[:success]).to eq t('sign_up.cancel.success') - end - - it 'does not destroy the user if the user is not in setup mode and is after 2fa' do - sign_in_as_user - - expect { delete :destroy }.to change(User, :count).by(0) - end - - it 'does not destroy the user if the user is not in setup mode and is before 2fa' do - sign_in_before_2fa - - expect { delete :destroy }.to change(User, :count).by(0) - end - - it 'redirects to the branded start page if the user came from an SP' do - session[:sp] = { issuer: 'http://localhost:3000', request_id: 'foo' } - - delete :destroy - - expect(response). - to redirect_to new_user_session_path(request_id: 'foo') - end - - it 'tracks the event in analytics when referer is nil' do - stub_analytics - properties = { request_came_from: 'no referer' } - - expect(@analytics).to receive(:track_event).with('Account Deletion Requested', properties) - - delete :destroy - end - - it 'tracks the event in analytics when referer is present' do - stub_analytics - request.env['HTTP_REFERER'] = 'http://example.com/' - properties = { request_came_from: 'users/sessions#new' } - - expect(@analytics).to receive(:track_event).with('Account Deletion Requested', properties) - - delete :destroy - end - - it 'calls ParseControllerFromReferer' do - expect_any_instance_of(ParseControllerFromReferer).to receive(:call).and_call_original - - delete :destroy - end - end -end diff --git a/spec/controllers/verify_controller_spec.rb b/spec/controllers/verify_controller_spec.rb index 38461cf851e..a9699062c70 100644 --- a/spec/controllers/verify_controller_spec.rb +++ b/spec/controllers/verify_controller_spec.rb @@ -18,6 +18,8 @@ end let(:profile) { subject.idv_session.profile } let(:step) { '' } + let(:sp) { build(:service_provider) } + let(:sp_session) { { issuer: sp.issuer } } subject(:response) { get :show, params: { step: step } } @@ -26,6 +28,7 @@ and_return(idv_api_enabled_steps) stub_sign_in(user) stub_idv_session + session[:sp] = sp_session if sp_session end it 'renders 404' do @@ -33,7 +36,8 @@ end context 'with idv api enabled' do - let(:idv_api_enabled_steps) { ['something'] } + let(:idv_api_enabled_steps) { ['password_confirm', 'personal_key', 'personal_key_confirm'] } + let(:step) { 'password_confirm' } context 'invalid step' do let(:step) { 'bad' } @@ -43,78 +47,29 @@ end end - context 'with personal key step enabled' do - let(:idv_api_enabled_steps) { ['personal_key', 'personal_key_confirm'] } - let(:step) { 'personal_key' } - - before do - profile_maker = Idv::ProfileMaker.new( - applicant: applicant, - user: user, - user_password: password, - ) - profile = profile_maker.save_profile - controller.idv_session.pii = profile_maker.pii_attributes - controller.idv_session.profile_id = profile.id - controller.idv_session.personal_key = profile.personal_key - end - - it 'renders view' do - expect(response).to render_template(:show) - end - - it 'sets app data' do - response - - expect(assigns[:app_data]).to include( - base_path: idv_app_path, - start_over_url: idv_session_path, - cancel_url: idv_cancel_path, - completion_url: idv_gpo_verify_url, - enabled_step_names: idv_api_enabled_steps, - initial_values: { 'personalKey' => kind_of(String) }, - store_key: kind_of(String), - ) - end - - context 'empty step' do - let(:step) { nil } + it 'renders view' do + expect(response).to render_template(:show) + end - it 'renders view' do - expect(response).to render_template(:show) - end - end + it 'sets app data' do + response + + expect(assigns[:app_data]).to include( + base_path: idv_app_path, + start_over_url: idv_session_path, + cancel_url: idv_cancel_path, + enabled_step_names: idv_api_enabled_steps, + initial_values: { 'userBundleToken' => kind_of(String) }, + store_key: kind_of(String), + ) end - context 'with password confirmation step enabled' do - let(:idv_api_enabled_steps) { ['password_confirm', 'personal_key', 'personal_key_confirm'] } - let(:step) { 'password_confirm' } + context 'empty step' do + let(:step) { nil } it 'renders view' do expect(response).to render_template(:show) end - - it 'sets app data' do - response - - expect(assigns[:app_data]).to include( - base_path: idv_app_path, - start_over_url: idv_session_path, - cancel_url: idv_cancel_path, - completion_url: account_url, - enabled_step_names: idv_api_enabled_steps, - initial_values: { 'userBundleToken' => kind_of(String) }, - store_key: kind_of(String), - ) - end - - context 'empty step' do - let(:step) { nil } - - it 'renders view' do - expect(response).to render_template(:show) - end - end end end @@ -122,7 +77,7 @@ def stub_idv_session idv_session = Idv::Session.new( user_session: controller.user_session, current_user: user, - service_provider: nil, + service_provider: sp, ) idv_session.applicant = applicant idv_session.resolution_successful = true diff --git a/spec/decorators/user_decorator_spec.rb b/spec/decorators/user_decorator_spec.rb index 1b9925d6df9..1885026e80e 100644 --- a/spec/decorators/user_decorator_spec.rb +++ b/spec/decorators/user_decorator_spec.rb @@ -295,6 +295,14 @@ before { active_profile.deactivate(:password_reset) } it { expect(decorated_user.password_reset_profile).to eq(active_profile) } + + context 'with a previously-cancelled pending profile' do + before do + user.profiles << build(:profile, :verification_cancelled) + end + + it { expect(decorated_user.password_reset_profile).to eq(active_profile) } + end end end end diff --git a/spec/factories/profiles.rb b/spec/factories/profiles.rb index f00067b82be..a8152e138e4 100644 --- a/spec/factories/profiles.rb +++ b/spec/factories/profiles.rb @@ -16,29 +16,21 @@ end trait :password_reset do + activated_at { Time.zone.now } + verified_at { Time.zone.now } deactivation_reason { :password_reset } end - trait :with_liveness do - proofing_components { { liveness_check: 'vendor' } } + trait :verification_cancelled do + deactivation_reason { :verification_cancelled } end - trait :with_pii do - pii do - Idp::Constants::DEFAULT_MOCK_PII_FROM_DOC.merge( - ssn: DocAuthHelper::GOOD_SSN, - phone: '+1 (555) 555-1234', - ) - end + trait :with_liveness do + proofing_components { { liveness_check: 'vendor' } } end trait :with_pii do - pii do - Idp::Constants::DEFAULT_MOCK_PII_FROM_DOC.merge( - ssn: DocAuthHelper::GOOD_SSN, - phone: '+1 (555) 555-1234', - ) - end + pii { Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE } end after(:build) do |profile, evaluator| diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 4119cbea806..16506e617a1 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -191,5 +191,13 @@ create(:profile, :active, :verified, :with_pii, user: user) end end + + trait :deactivated_password_reset_profile do + signed_up + + after :build do |user| + create(:profile, :password_reset, :with_pii, user: user) + end + end end end diff --git a/spec/features/accessibility/user_pages_spec.rb b/spec/features/accessibility/user_pages_spec.rb index d96d5120b57..788fe3e789a 100644 --- a/spec/features/accessibility/user_pages_spec.rb +++ b/spec/features/accessibility/user_pages_spec.rb @@ -37,7 +37,7 @@ scenario 'two factor options page' do sign_up_and_set_password - expect(current_path).to eq(two_factor_options_path) + expect(current_path).to eq(authentication_methods_setup_path) expect(page).to be_axe_clean.according_to :section508, :"best-practice", :wcag21aa expect(page).to label_required_fields expect(page).to be_uniquely_titled diff --git a/spec/features/idv/cancel_spec.rb b/spec/features/idv/cancel_spec.rb new file mode 100644 index 00000000000..f8120adaf7f --- /dev/null +++ b/spec/features/idv/cancel_spec.rb @@ -0,0 +1,84 @@ +require 'rails_helper' + +describe 'cancel IdV', :js do + include IdvStepHelper + include DocAuthHelper + + let(:sp) { nil } + let(:fake_analytics) { FakeAnalytics.new } + + before do + start_idv_from_sp(sp) + sign_in_and_2fa_user + complete_doc_auth_steps_before_agreement_step + allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) + end + + it 'shows the user a cancellation message with the option to go back to the step' do + original_path = current_path + + click_link t('links.cancel') + + expect(page).to have_content(t('headings.cancellations.prompt')) + expect(current_path).to eq(idv_cancel_path) + expect(fake_analytics).to have_logged_event('IdV: cancellation visited', step: 'agreement') + + click_on t('links.go_back') + + expect(current_path).to eq(original_path) + expect(fake_analytics).to have_logged_event('IdV: cancellation go back', step: 'agreement') + end + + it 'shows a cancellation message with option to cancel and reset idv' do + click_link t('links.cancel') + + expect(page).to have_content(t('headings.cancellations.prompt')) + expect(current_path).to eq(idv_cancel_path) + expect(fake_analytics).to have_logged_event('IdV: cancellation visited', step: 'agreement') + + click_on t('forms.buttons.cancel') + + expect(page).to have_content(t('headings.cancellations.confirmation', app_name: APP_NAME)) + expect(current_path).to eq(idv_cancel_path) + expect(page).to have_link( + "‹ #{t('links.back_to_sp', sp: t('links.my_account'))}", + href: account_url, + ) + expect(fake_analytics).to have_logged_event('IdV: cancellation confirmed', step: 'agreement') + + # After visiting /verify, expect to redirect to the first step in the IdV flow. + visit idv_path + expect(current_path).to eq(idv_doc_auth_step_path(step: :welcome)) + end + + context 'with an sp' do + let(:sp) { :oidc } + + it 'shows the user a cancellation message with the option to cancel and reset idv' do + sp_name = 'Test SP' + allow_any_instance_of(ServiceProviderSessionDecorator).to receive(:sp_name). + and_return(sp_name) + + click_link t('links.cancel') + + expect(page).to have_content(t('headings.cancellations.prompt')) + expect(current_path).to eq(idv_cancel_path) + expect(fake_analytics).to have_logged_event('IdV: cancellation visited', step: 'agreement') + + click_on t('forms.buttons.cancel') + + expect(page).to have_content(t('headings.cancellations.confirmation', app_name: APP_NAME)) + expect(current_path).to eq(idv_cancel_path) + expect(fake_analytics).to have_logged_event('IdV: cancellation confirmed', step: 'agreement') + + expect(page).to have_link( + "‹ #{t('links.back_to_sp', sp: sp_name)}", + href: return_to_sp_failure_to_proof_path(step: 'agreement', location: 'cancel'), + ) + + # After visiting /verify, expect to redirect to the first step in the IdV flow. + visit idv_path + expect(current_path).to eq(idv_doc_auth_step_path(step: :welcome)) + end + end +end diff --git a/spec/features/idv/doc_auth/cancel_spec.rb b/spec/features/idv/doc_auth/cancel_spec.rb deleted file mode 100644 index 0514a030bdf..00000000000 --- a/spec/features/idv/doc_auth/cancel_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'rails_helper' - -feature 'doc auth cancel' do - include IdvStepHelper - include DocAuthHelper - - before do - sign_in_and_2fa_user - complete_doc_auth_steps_before_verify_step - end - - it 'correctly restarts doc auth flow upon cancel and revisit' do - expect(page).to have_current_path(idv_doc_auth_verify_step) - - click_link t('links.cancel') - - expect(page).to have_current_path(idv_cancel_path(step: 'verify')) - - click_button t('forms.buttons.cancel') - - expect(page).to have_content(t('headings.cancellations.confirmation', app_name: APP_NAME)) - expect(current_path).to eq(idv_cancel_path) - - visit account_path - expect(current_path).to eq(account_path) - - visit(idv_doc_auth_verify_step) - expect(current_path).to eq(idv_doc_auth_welcome_step) - end -end diff --git a/spec/features/idv/doc_auth/document_capture_step_spec.rb b/spec/features/idv/doc_auth/document_capture_step_spec.rb index 2ddab9ff766..7881d37c007 100644 --- a/spec/features/idv/doc_auth/document_capture_step_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_step_spec.rb @@ -55,20 +55,19 @@ ) end - it 'shows the selfie upload option' do + it 'shows expected content' do + # Selfie upload option expect(page).to have_content(t('doc_auth.headings.document_capture_selfie')) - end - it 'displays doc capture tips' do + # Document capture tips expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_header_text')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text1')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text2')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text3')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text4')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_hint')) - end - it 'displays selfie tips' do + # Selfie tips expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text1')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text2')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text3')) diff --git a/spec/features/idv/doc_auth/verify_step_spec.rb b/spec/features/idv/doc_auth/verify_step_spec.rb index 88679951e13..607cfc18894 100644 --- a/spec/features/idv/doc_auth/verify_step_spec.rb +++ b/spec/features/idv/doc_auth/verify_step_spec.rb @@ -159,11 +159,10 @@ success: true, errors: {}, context: { stages: [] }, ) - stub_const( - 'Idv::Steps::VerifyBaseStep::AAMVA_SUPPORTED_JURISDICTIONS', - Idv::Steps::VerifyBaseStep::AAMVA_SUPPORTED_JURISDICTIONS + - [Idp::Constants::DEFAULT_MOCK_PII_FROM_DOC[:state_id_jurisdiction]], + allow(IdentityConfig.store).to receive(:aamva_supported_jurisdictions).and_return( + [Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction]], ) + sign_in_and_2fa_user complete_doc_auth_steps_before_verify_step click_idv_continue @@ -182,11 +181,11 @@ success: true, errors: {}, context: { stages: [] }, ) - stub_const( - 'Idv::Steps::VerifyBaseStep::AAMVA_SUPPORTED_JURISDICTIONS', - Idv::Steps::VerifyBaseStep::AAMVA_SUPPORTED_JURISDICTIONS - - [Idp::Constants::DEFAULT_MOCK_PII_FROM_DOC[:state_id_jurisdiction]], + allow(IdentityConfig.store).to receive(:aamva_supported_jurisdictions).and_return( + IdentityConfig.store.aamva_supported_jurisdictions - + [Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction]], ) + sign_in_and_2fa_user complete_doc_auth_steps_before_verify_step click_idv_continue diff --git a/spec/features/idv/doc_capture/document_capture_step_spec.rb b/spec/features/idv/doc_capture/document_capture_step_spec.rb index adc8b35965f..d466f01b729 100644 --- a/spec/features/idv/doc_capture/document_capture_step_spec.rb +++ b/spec/features/idv/doc_capture/document_capture_step_spec.rb @@ -196,16 +196,16 @@ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) end - it 'displays doc capture tips' do + it 'displays expected content' do + # Doc capture tips expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_header_text')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text1')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text2')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text3')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text4')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_hint')) - end - it 'does not display selfie tips' do + # No selfie tips expect(page).not_to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text1')) expect(page).not_to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text2')) expect(page).not_to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text3')) @@ -218,20 +218,19 @@ expect(page).to have_content(t('doc_auth.headings.document_capture_back')) end - it 'shows the selfie upload option' do + it 'displays expected content' do + # Selfie upload option expect(page).to have_content(t('doc_auth.headings.document_capture_selfie')) - end - it 'displays doc capture tips' do + # Doc capture tips expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_header_text')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text1')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text2')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text3')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text4')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_hint')) - end - it 'displays selfie tips' do + # Selfie tips expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text1')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text2')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text3')) @@ -331,20 +330,19 @@ ) end - it 'does not show the selfie upload option' do + it 'displays expected content' do + # No selfie upload option expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) - end - it 'displays document capture tips' do + # Document capture tips expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_header_text')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text1')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text2')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text3')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text4')) expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_hint')) - end - it 'does not display selfie tips' do + # No selfie tips expect(page).not_to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text1')) expect(page).not_to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text2')) expect(page).not_to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text3')) diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb index c337b1b0fda..8b4406ae9a4 100644 --- a/spec/features/idv/in_person_spec.rb +++ b/spec/features/idv/in_person_spec.rb @@ -5,7 +5,7 @@ include IdvHelper it 'works for a happy path', js: true, allow_browser_log: true do - sign_in_and_2fa_user + user = sign_in_and_2fa_user # welcome step visit idv_doc_auth_welcome_step # only thing used from DocAuthHelper @@ -52,22 +52,20 @@ click_idv_continue # phone page - expect(page).to have_content(t('in_person_proofing.headings.phone')) + expect(page).to have_content(t('idv.titles.session.phone')) + fill_out_phone_form_mfa_phone(user) click_idv_continue # password confirm page expect(page).to have_content( - t('in_person_proofing.headings.password_confirm', app_name: APP_NAME), + t('idv.titles.session.review', app_name: APP_NAME), ) + fill_in t('idv.form.password'), with: Features::SessionHelper::VALID_PASSWORD click_idv_continue # personal key page - expect(page).to have_content(t('in_person_proofing.headings.personal_key')) - click_idv_continue - - # barcode page - expect(page).to have_content(t('in_person_proofing.headings.barcode')) - click_idv_continue + expect(page).to have_content(t('titles.idv.personal_key')) + acknowledge_and_confirm_personal_key # returns to account page expect(page).to have_content(t('headings.account.login_info')) diff --git a/spec/features/idv/steps/confirmation_step_spec.rb b/spec/features/idv/steps/confirmation_step_spec.rb index 165e98c8e6e..6fa9a067290 100644 --- a/spec/features/idv/steps/confirmation_step_spec.rb +++ b/spec/features/idv/steps/confirmation_step_spec.rb @@ -28,7 +28,7 @@ context 'with idv app feature enabled' do before do allow(IdentityConfig.store).to receive(:idv_api_enabled_steps). - and_return(['personal_key', 'personal_key_confirm']) + and_return(['password_confirm', 'personal_key', 'personal_key_confirm']) end it_behaves_like 'idv confirmation step' diff --git a/spec/features/idv/steps/phone_otp_delivery_selection_step_spec.rb b/spec/features/idv/steps/phone_otp_delivery_selection_step_spec.rb index 04802285ed1..d639b17dce4 100644 --- a/spec/features/idv/steps/phone_otp_delivery_selection_step_spec.rb +++ b/spec/features/idv/steps/phone_otp_delivery_selection_step_spec.rb @@ -110,9 +110,4 @@ expect(page).to have_content(I18n.t('telephony.error.friendly_message.invalid_calling_area')) expect(page).to have_current_path(idv_phone_path) end - - context 'cancelling IdV' do - it_behaves_like 'cancel at idv step', :phone_otp_delivery_selection - it_behaves_like 'cancel at idv step', :phone_otp_delivery_selection, :oidc - end end diff --git a/spec/features/idv/steps/phone_otp_verification_step_spec.rb b/spec/features/idv/steps/phone_otp_verification_step_spec.rb index 07295d2a3bb..d424a51c9e1 100644 --- a/spec/features/idv/steps/phone_otp_verification_step_spec.rb +++ b/spec/features/idv/steps/phone_otp_verification_step_spec.rb @@ -97,9 +97,4 @@ expect(page).to have_content(calling_area_error.friendly_message) expect(page).to have_current_path(idv_phone_path) end - - context 'cancelling IdV' do - it_behaves_like 'cancel at idv step', :phone_otp_verification - it_behaves_like 'cancel at idv step', :phone_otp_verification, :oidc - end end diff --git a/spec/features/idv/steps/phone_step_spec.rb b/spec/features/idv/steps/phone_step_spec.rb index 23337e0f9b5..712e0f23935 100644 --- a/spec/features/idv/steps/phone_step_spec.rb +++ b/spec/features/idv/steps/phone_step_spec.rb @@ -148,11 +148,6 @@ it_behaves_like 'async timed out' end - context 'cancelling IdV' do - it_behaves_like 'cancel at idv step', :phone - it_behaves_like 'cancel at idv step', :phone, :oidc - end - context "when the user's information cannot be verified" do it_behaves_like 'fail to verify idv info', :phone diff --git a/spec/features/idv/steps/review_step_spec.rb b/spec/features/idv/steps/review_step_spec.rb index 8c0ce2dbd57..90343834f46 100644 --- a/spec/features/idv/steps/review_step_spec.rb +++ b/spec/features/idv/steps/review_step_spec.rb @@ -145,9 +145,4 @@ expect(page).to have_current_path(idv_app_path(step: :personal_key)) end end - - context 'cancelling IdV' do - it_behaves_like 'cancel at idv step', :review - it_behaves_like 'cancel at idv step', :review, :oidc - end end diff --git a/spec/features/openid_connect/aal3_required_spec.rb b/spec/features/openid_connect/aal3_required_spec.rb index 78d6795c931..1d7e77734c5 100644 --- a/spec/features/openid_connect/aal3_required_spec.rb +++ b/spec/features/openid_connect/aal3_required_spec.rb @@ -11,7 +11,7 @@ visit_idp_from_ial1_oidc_sp_requesting_aal3(prompt: 'select_account') sign_in_live_with_2fa(user) - expect(current_url).to eq(two_factor_options_url) + expect(current_url).to eq(authentication_methods_setup_url) expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice')) expect(page).to have_xpath("//img[@alt='important alert icon']") end @@ -43,7 +43,7 @@ visit_idp_from_ial1_oidc_sp_defaulting_to_aal3(prompt: 'select_account') sign_in_live_with_2fa(user) - expect(current_url).to eq(two_factor_options_url) + expect(current_url).to eq(authentication_methods_setup_url) expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice')) expect(page).to have_xpath("//img[@alt='important alert icon']") end diff --git a/spec/features/phone/confirmation_spec.rb b/spec/features/phone/confirmation_spec.rb index faf84bb34cc..ad503c58c32 100644 --- a/spec/features/phone/confirmation_spec.rb +++ b/spec/features/phone/confirmation_spec.rb @@ -28,7 +28,7 @@ def expect_successful_otp_confirmation(delivery_method) def expect_failed_otp_confirmation(_delivery_method) visit account_path - expect(current_path).to eq(two_factor_options_path) + expect(current_path).to eq(authentication_methods_setup_path) expect(phone_configuration).to be_nil end end diff --git a/spec/features/saml/aal3_required_spec.rb b/spec/features/saml/aal3_required_spec.rb index 2b72e43d460..1f56478d872 100644 --- a/spec/features/saml/aal3_required_spec.rb +++ b/spec/features/saml/aal3_required_spec.rb @@ -21,7 +21,7 @@ sign_in_and_2fa_user(user_with_2fa) visit_saml_authn_request_url(overrides: { issuer: aal3_issuer, authn_context: nil }) - expect(current_url).to eq(two_factor_options_url) + expect(current_url).to eq(authentication_methods_setup_url) expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice')) expect(page).to have_xpath("//img[@alt='important alert icon']") end diff --git a/spec/features/saml/ial1_sso_spec.rb b/spec/features/saml/ial1_sso_spec.rb index c61b645dd4c..6608886644f 100644 --- a/spec/features/saml/ial1_sso_spec.rb +++ b/spec/features/saml/ial1_sso_spec.rb @@ -148,7 +148,7 @@ visit saml_authn_request_url - expect(current_path).to eq two_factor_options_path + expect(current_path).to eq authentication_methods_setup_path end end diff --git a/spec/features/saml/saml_spec.rb b/spec/features/saml/saml_spec.rb index 9b8a2df2485..0905d80b170 100644 --- a/spec/features/saml/saml_spec.rb +++ b/spec/features/saml/saml_spec.rb @@ -74,7 +74,7 @@ class MockSession; end end it 'prompts the user to set up 2FA' do - expect(current_path).to eq two_factor_options_path + expect(current_path).to eq authentication_methods_setup_path end it 'prompts the user to confirm phone after setting up 2FA' do diff --git a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb index 44f35030338..fb0e496ab2f 100644 --- a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb +++ b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb @@ -10,7 +10,7 @@ scenario 'user can set up 2 MFA methods properly' do sign_in_before_2fa - expect(current_path).to eq two_factor_options_path + expect(current_path).to eq authentication_methods_setup_path click_2fa_option('phone') click_2fa_option('backup_code') @@ -43,7 +43,7 @@ scenario 'user can select 1 MFA methods and will be prompted to add another method' do sign_in_before_2fa - expect(current_path).to eq two_factor_options_path + expect(current_path).to eq authentication_methods_setup_path click_2fa_option('backup_code') @@ -67,6 +67,38 @@ expect(current_path).to eq account_path end + + scenario 'user can select 1 MFA methods and cancels selecting second mfa' do + sign_in_before_2fa + + expect(current_path).to eq authentication_methods_setup_path + + click_2fa_option('backup_code') + + click_continue + + expect(current_path).to eq backup_code_setup_path + + click_continue + + expect(page).to have_link(t('forms.backup_code.download')) + + click_continue + + expect(page).to have_content(t('notices.backup_codes_configured')) + + expect(page).to have_current_path( + auth_method_confirmation_path, + ) + + click_link t('mfa.add') + + expect(page).to have_current_path(second_mfa_setup_path) + + click_link t('links.cancel') + + expect(page).to have_current_path(account_path) + end end describe 'user attempts to submit with only the phone MFA method selected', js: true do @@ -81,7 +113,7 @@ to have_content(t('errors.two_factor_auth_setup.must_select_additional_option')) expect( URI.parse(current_url).path + '#' + URI.parse(current_url).fragment, - ).to eq two_factor_options_path(anchor: 'select_phone') + ).to eq authentication_methods_setup_path(anchor: 'select_phone') end scenario 'clears the error when another mfa method is selected' do diff --git a/spec/features/two_factor_authentication/sign_in_spec.rb b/spec/features/two_factor_authentication/sign_in_spec.rb index 78d898b5c91..a91ff93fd0d 100644 --- a/spec/features/two_factor_authentication/sign_in_spec.rb +++ b/spec/features/two_factor_authentication/sign_in_spec.rb @@ -7,7 +7,7 @@ attempt_to_bypass_2fa_setup - expect(current_path).to eq two_factor_options_path + expect(current_path).to eq authentication_methods_setup_path select_2fa_option('phone') @@ -55,7 +55,7 @@ click_on t('two_factor_authentication.choose_another_option') - expect(current_path).to eq two_factor_options_path + expect(current_path).to eq authentication_methods_setup_path end end diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb index 3b8ae651141..0003f022f1b 100644 --- a/spec/features/users/sign_in_spec.rb +++ b/spec/features/users/sign_in_spec.rb @@ -143,7 +143,7 @@ submit_form_with_valid_email(email) click_confirmation_link_in_email(email) submit_form_with_valid_password - expect(page).to have_current_path(two_factor_options_path) + expect(page).to have_current_path(authentication_methods_setup_path) select_2fa_option('phone') fill_in :new_phone_form_phone, with: '2025551314' @@ -930,7 +930,7 @@ fill_in_code_with_last_phone_otp click_submit_default - expect(current_path).to eq two_factor_options_path + expect(current_path).to eq authentication_methods_setup_path select_2fa_option('piv_cac') expect(page).to have_current_path setup_piv_cac_path diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb index aaa40ea4cc6..8bb943b45b8 100644 --- a/spec/features/users/sign_up_spec.rb +++ b/spec/features/users/sign_up_spec.rb @@ -124,7 +124,7 @@ Throttle.attempt_window_in_minutes(:phone_confirmation).minutes, ) - expect(current_path).to eq(two_factor_options_path) + expect(current_path).to eq(authentication_methods_setup_path) expect(page).to have_content( I18n.t( 'errors.messages.phone_confirmation_throttled', @@ -303,7 +303,7 @@ click_confirmation_link_in_email(email) submit_form_with_valid_password - expect(page).to have_current_path(two_factor_options_path) + expect(page).to have_current_path(authentication_methods_setup_path) end end end @@ -322,7 +322,7 @@ it 'redirects back with an error if the user does not select 2FA option' do sign_in_user - visit two_factor_options_path + visit authentication_methods_setup_path click_on 'Continue' expect(page).to have_content(t('errors.two_factor_auth_setup.must_select_option')) diff --git a/spec/features/visitors/email_confirmation_spec.rb b/spec/features/visitors/email_confirmation_spec.rb index 7ad04872442..622efcc2f32 100644 --- a/spec/features/visitors/email_confirmation_spec.rb +++ b/spec/features/visitors/email_confirmation_spec.rb @@ -17,7 +17,7 @@ fill_in t('forms.password'), with: Features::SessionHelper::VALID_PASSWORD click_button t('forms.buttons.continue') - expect(current_url).to eq two_factor_options_url + expect(current_url).to eq authentication_methods_setup_url expect(page).to_not have_content t('devise.confirmations.confirmed_but_must_set_password') end diff --git a/spec/features/visitors/password_recovery_spec.rb b/spec/features/visitors/password_recovery_spec.rb index 131bf942438..8d2ec6fe37f 100644 --- a/spec/features/visitors/password_recovery_spec.rb +++ b/spec/features/visitors/password_recovery_spec.rb @@ -54,7 +54,7 @@ fill_in_credentials_and_submit(user.email, 'NewVal!dPassw0rd') - expect(current_path).to eq two_factor_options_path + expect(current_path).to eq authentication_methods_setup_path end end @@ -91,7 +91,7 @@ it 'prompts user to set up their 2FA options after signing back in' do reset_password_and_sign_back_in(@user) - expect(current_path).to eq two_factor_options_path + expect(current_path).to eq authentication_methods_setup_path end end diff --git a/spec/features/visitors/set_password_spec.rb b/spec/features/visitors/set_password_spec.rb index 9cffa3706a7..2b505cd909d 100644 --- a/spec/features/visitors/set_password_spec.rb +++ b/spec/features/visitors/set_password_spec.rb @@ -37,25 +37,17 @@ confirm_last_user end - it 'is visible on page (not have "display-none" class)' do - expect(page).to_not have_css('#pw-strength-cntnr.display-none') - end - - it 'updates as password changes' do + it 'updates strength feedback as password changes' do expect(page).to have_content '...' fill_in t('forms.password'), with: 'password' expect(page).to have_content 'Very weak' - fill_in t('forms.password'), with: 'this is a great sentence' - expect(page).to have_content 'Great!' - end - - it 'has dynamic password strength feedback' do - expect(page).to have_content '...' - fill_in t('forms.password'), with: '123456789' expect(page).to have_content t('zxcvbn.feedback.this_is_a_top_10_common_password') + + fill_in t('forms.password'), with: 'this is a great sentence' + expect(page).to have_content 'Great!' end end diff --git a/spec/features/webauthn/sign_up_spec.rb b/spec/features/webauthn/sign_up_spec.rb index 8446481ef18..c421c1c284f 100644 --- a/spec/features/webauthn/sign_up_spec.rb +++ b/spec/features/webauthn/sign_up_spec.rb @@ -18,7 +18,7 @@ def expect_webauthn_setup_success def expect_webauthn_setup_error expect(page).to have_content t('errors.webauthn_setup.general_error') - expect(page).to have_current_path(two_factor_options_path) + expect(page).to have_current_path(authentication_methods_setup_path) end it_behaves_like 'webauthn setup' diff --git a/spec/forms/verify_password_form_spec.rb b/spec/forms/verify_password_form_spec.rb index b46008443f3..d6c0fbeae10 100644 --- a/spec/forms/verify_password_form_spec.rb +++ b/spec/forms/verify_password_form_spec.rb @@ -7,7 +7,7 @@ password = 'cab123DZN456' user = create(:user, password: password) pii = { ssn: '111111111' } - create(:profile, user: user, deactivation_reason: :password_reset, pii: pii) + create(:profile, :password_reset, user: user, pii: pii) form = VerifyPasswordForm.new( user: user, password: password, @@ -25,7 +25,7 @@ password = 'cab123DZN456' user = create(:user, password: password) pii = { ssn: '111111111' } - create(:profile, user: user, deactivation_reason: :password_reset, pii: pii) + create(:profile, :password_reset, user: user, pii: pii) form = VerifyPasswordForm.new( user: user, password: "#{password}a", diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index dff0b640f51..a57181a1589 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -95,7 +95,7 @@ create( :profile, :verified, - deactivation_reason: :password_reset, + :password_reset, ), ], ) @@ -115,7 +115,7 @@ :profile, :active, :verified, - deactivation_reason: :password_reset, + :password_reset, proofing_components: { liveness_check: DocAuthRouter.doc_auth_vendor, address_check: :lexis_nexis_address, diff --git a/spec/jobs/remove_old_throttles_job_spec.rb b/spec/jobs/remove_old_throttles_job_spec.rb deleted file mode 100644 index c912f5239e1..00000000000 --- a/spec/jobs/remove_old_throttles_job_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'rails_helper' - -RSpec.describe RemoveOldThrottlesJob do - let(:limit) { 1 } - let(:total_limit) { 10 } - let(:now) { Time.zone.now } - - subject(:job) { RemoveOldThrottlesJob.new } - - describe '#perform' do - subject(:perform) { job.perform(now, limit: limit, total_limit: total_limit) } - - it 'deletes throttles that are older than WINDOW' do - _old_throttle = create( - :database_throttle, - target: SecureRandom.hex, - updated_at: now - 45.days, - ) - new_throttle = create(:database_throttle, target: SecureRandom.hex, updated_at: now) - - perform - - expect(DatabaseThrottle.all.map(&:id)).to match_array([new_throttle.id]) - end - - it 'deletes legacy rows with updated_at: nil' do - legacy_throttle = create(:database_throttle, target: SecureRandom.hex) - legacy_throttle.update(updated_at: nil) - expect(legacy_throttle.reload.updated_at).to be_nil - - expect { perform }.to(change { DatabaseThrottle.count }.to(0)) - end - - it 'stops after total_limit jobs' do - (total_limit + 1).times do - create(:database_throttle, target: SecureRandom.hex, updated_at: now - 45.days) - end - - expect { perform }.to(change { DatabaseThrottle.count }.to(1)) - end - end - - describe '#good_job_concurrency_key' do - it 'is the job name and the current time, rounded to the nearest hour' do - now = Time.zone.at(1629817200) - - job_now = RemoveOldThrottlesJob.new(now) - expect(job_now.good_job_concurrency_key).to eq("remove-old-throttles-#{now.to_i}") - - job_plus_30m = RemoveOldThrottlesJob.new(now + 30.minutes) - expect(job_plus_30m.good_job_concurrency_key).to eq(job_now.good_job_concurrency_key) - - job_plus_1h = RemoveOldThrottlesJob.new(now + 1.hour) - expect(job_plus_1h.good_job_concurrency_key).to_not eq(job_now.good_job_concurrency_key) - end - end -end diff --git a/spec/jobs/reports/agency_invoice_iaa_supplement_report_spec.rb b/spec/jobs/reports/agency_invoice_iaa_supplement_report_spec.rb index bf659fe3bb8..0af4b89e442 100644 --- a/spec/jobs/reports/agency_invoice_iaa_supplement_report_spec.rb +++ b/spec/jobs/reports/agency_invoice_iaa_supplement_report_spec.rb @@ -97,6 +97,7 @@ ial: 1, requested_at: inside_iaa1, returned_at: inside_iaa1, + billable: true, ) # 1 unique user in month 1 at IAA 2 @ IAL 2 diff --git a/spec/jobs/reports/combined_invoice_supplement_report_spec.rb b/spec/jobs/reports/combined_invoice_supplement_report_spec.rb index f34d6133c10..e95582d36b7 100644 --- a/spec/jobs/reports/combined_invoice_supplement_report_spec.rb +++ b/spec/jobs/reports/combined_invoice_supplement_report_spec.rb @@ -107,6 +107,7 @@ ial: 1, requested_at: inside_iaa1, returned_at: inside_iaa1, + billable: true, ) # 1 unique user in month 1 at IAA 2 sp 1 @ IAL 2 diff --git a/spec/lib/session_encryptor_spec.rb b/spec/lib/session_encryptor_spec.rb index 7c5d56cc8dc..cd1c6742393 100644 --- a/spec/lib/session_encryptor_spec.rb +++ b/spec/lib/session_encryptor_spec.rb @@ -171,7 +171,7 @@ it 'raises if sensitive value is not KMS encrypted' do session = { - 'new_key' => Idp::Constants::DEFAULT_MOCK_PII_FROM_DOC[:last_name], + 'new_key' => Idp::Constants::MOCK_IDV_APPLICANT[:last_name], } expect { diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 4310f274a54..24ea6542c2e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -11,7 +11,6 @@ it { is_expected.to have_many(:phone_configurations) } it { is_expected.to have_many(:webauthn_configurations) } it { is_expected.to have_one(:proofing_component) } - it { is_expected.to have_many(:database_throttles) } end it 'does not send an email when #create is called' do @@ -377,13 +376,13 @@ user = User.new _old_profile = create( :profile, - deactivation_reason: :verification_pending, + :verification_pending, created_at: 1.day.ago, user: user, ) new_profile = create( :profile, - deactivation_reason: :verification_pending, + :verification_pending, user: user, ) @@ -396,7 +395,7 @@ user = User.new create( :profile, - deactivation_reason: :password_reset, + :password_reset, created_at: 1.day.ago, user: user, ) diff --git a/spec/presenters/cancellation_presenter_spec.rb b/spec/presenters/cancellation_presenter_spec.rb index d56f7794acc..3200f402a01 100644 --- a/spec/presenters/cancellation_presenter_spec.rb +++ b/spec/presenters/cancellation_presenter_spec.rb @@ -8,7 +8,7 @@ subject { described_class.new(referer: referer_header, url_options: {}) } describe '#go_back_link' do - let(:sign_up_path) { '/two_factor_options' } + let(:sign_up_path) { '/authentication_methods_setup' } context 'without a referer header' do let(:referer_header) { nil } diff --git a/spec/presenters/navigation_presenter_spec.rb b/spec/presenters/navigation_presenter_spec.rb index 2e4badc2880..035567e49ec 100644 --- a/spec/presenters/navigation_presenter_spec.rb +++ b/spec/presenters/navigation_presenter_spec.rb @@ -8,7 +8,7 @@ describe '#navigation_items' do describe 'personal key' do context 'for a user with a personal key' do - let(:user) { build(:user, personal_key: 'abcdef') } + let(:user) { build(:user, :proofed) } it 'has a link to reset personal key' do nav_item = navigation.navigation_items.first.children.last @@ -25,6 +25,18 @@ expect(has_reset_personal_key).to eq(false) end end + + context 'for a proofed user with deactivated profile due to password reset' do + let(:user) { build(:user, :deactivated_password_reset_profile) } + + it 'does not have a link to reset personal key' do + has_reset_personal_key = navigation.navigation_items.first.children.any? do |item| + item.title == I18n.t('account.navigation.reset_personal_key') + end + + expect(has_reset_personal_key).to eq(false) + end + end end end end diff --git a/spec/services/analytics_spec.rb b/spec/services/analytics_spec.rb index 4b5ac42f887..dc02d7e9c97 100644 --- a/spec/services/analytics_spec.rb +++ b/spec/services/analytics_spec.rb @@ -186,7 +186,7 @@ it 'does not alert when pii values are inside words' do expect(ahoy).to receive(:track) - stub_const('Idp::Constants::DEFAULT_MOCK_PII_FROM_DOC', zipcode: '12345') + stub_const('Idp::Constants::MOCK_IDV_APPLICANT', zipcode: '12345') expect do analytics.track_event( diff --git a/spec/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window_spec.rb b/spec/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window_spec.rb index 5097f66776e..e73cdde3894 100644 --- a/spec/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window_spec.rb +++ b/spec/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window_spec.rb @@ -49,9 +49,21 @@ service_provider: service_provider, requested_at: partial_month_date, returned_at: partial_month_date, + billable: true, ) end + # non-billable event during partial month, should be ignored + create( + :sp_return_log, + user: user, + ial: 1, + service_provider: service_provider, + requested_at: partial_month_date, + returned_at: partial_month_date, + billable: false, + ) + # 11 IAL 1 auths during full month create( :monthly_sp_auth_count, diff --git a/spec/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa_spec.rb b/spec/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa_spec.rb index 3f223aae845..cb20ee443a0 100644 --- a/spec/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa_spec.rb +++ b/spec/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa_spec.rb @@ -61,6 +61,18 @@ ial: 1, requested_at: inside_partial_month, returned_at: inside_partial_month, + billable: true, + ) + + # non-billable event in partial month, should be ignored + create( + :sp_return_log, + user_id: user1.id, + issuer: issuer1, + ial: 1, + requested_at: inside_partial_month, + returned_at: inside_partial_month, + billable: false, ) # 2 unique user in partial month @ IAL2 @@ -72,6 +84,7 @@ ial: 2, requested_at: inside_partial_month, returned_at: inside_partial_month, + billable: true, ) end diff --git a/spec/services/doc_auth/mock/result_response_builder_spec.rb b/spec/services/doc_auth/mock/result_response_builder_spec.rb index 617aaba227f..73d92defa75 100644 --- a/spec/services/doc_auth/mock/result_response_builder_spec.rb +++ b/spec/services/doc_auth/mock/result_response_builder_spec.rb @@ -24,7 +24,7 @@ expect(response.errors).to eq({}) expect(response.exception).to eq(nil) expect(response.pii_from_doc). - to eq(Idp::Constants::DEFAULT_MOCK_PII_FROM_DOC) + to eq(Idp::Constants::MOCK_IDV_APPLICANT) end end @@ -178,7 +178,7 @@ expect(response.errors).to eq({}) expect(response.exception).to eq(nil) expect(response.pii_from_doc). - to eq(Idp::Constants::DEFAULT_MOCK_PII_FROM_DOC) + to eq(Idp::Constants::MOCK_IDV_APPLICANT) end end diff --git a/spec/services/idv/steps/ipp/barcode_step_spec.rb b/spec/services/idv/steps/ipp/barcode_step_spec.rb deleted file mode 100644 index cc6641a418e..00000000000 --- a/spec/services/idv/steps/ipp/barcode_step_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'rails_helper' - -describe Idv::Steps::Ipp::BarcodeStep do - let(:user) { build(:user) } - let(:service_provider) { create(:service_provider) } - let(:controller) do - instance_double( - 'controller', - session: { sp: { issuer: service_provider.issuer } }, - current_user: user, - ) - end - - let(:flow) do - Idv::Flows::InPersonFlow.new(controller, {}, 'idv/in_person') - end - - subject(:step) do - Idv::Steps::Ipp::BarcodeStep.new(flow) - end - - describe '#call' do - it 'works' do - result = step.call - expect(result).to be_nil - end - end -end diff --git a/spec/services/idv/steps/ipp/password_confirm_step_spec.rb b/spec/services/idv/steps/ipp/password_confirm_step_spec.rb deleted file mode 100644 index 582091a3a75..00000000000 --- a/spec/services/idv/steps/ipp/password_confirm_step_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'rails_helper' - -describe Idv::Steps::Ipp::PasswordConfirmStep do - let(:user) { build(:user) } - let(:service_provider) { create(:service_provider) } - let(:controller) do - instance_double( - 'controller', - session: { sp: { issuer: service_provider.issuer } }, - current_user: user, - ) - end - - let(:flow) do - Idv::Flows::InPersonFlow.new(controller, {}, 'idv/in_person') - end - - subject(:step) do - Idv::Steps::Ipp::PasswordConfirmStep.new(flow) - end - - describe '#call' do - it 'works' do - result = step.call - expect(result).to be_nil - end - end -end diff --git a/spec/services/idv/steps/ipp/personal_key_step_spec.rb b/spec/services/idv/steps/ipp/personal_key_step_spec.rb deleted file mode 100644 index a667e1f5603..00000000000 --- a/spec/services/idv/steps/ipp/personal_key_step_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'rails_helper' - -describe Idv::Steps::Ipp::PersonalKeyStep do - let(:user) { build(:user) } - let(:service_provider) { create(:service_provider) } - let(:controller) do - instance_double( - 'controller', - session: { sp: { issuer: service_provider.issuer } }, - current_user: user, - ) - end - - let(:flow) do - Idv::Flows::InPersonFlow.new(controller, {}, 'idv/in_person') - end - - subject(:step) do - Idv::Steps::Ipp::PersonalKeyStep.new(flow) - end - - describe '#call' do - it 'works' do - result = step.call - expect(result).to be_nil - end - end -end diff --git a/spec/services/idv/steps/ipp/phone_step_spec.rb b/spec/services/idv/steps/ipp/phone_step_spec.rb deleted file mode 100644 index 28fcb3a4ecd..00000000000 --- a/spec/services/idv/steps/ipp/phone_step_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'rails_helper' - -describe Idv::Steps::Ipp::PhoneStep do - let(:user) { build(:user) } - let(:service_provider) { create(:service_provider) } - let(:controller) do - instance_double( - 'controller', - session: { sp: { issuer: service_provider.issuer } }, - current_user: user, - ) - end - - let(:flow) do - Idv::Flows::InPersonFlow.new(controller, {}, 'idv/in_person') - end - - subject(:step) do - Idv::Steps::Ipp::PhoneStep.new(flow) - end - - describe '#call' do - it 'works' do - result = step.call - expect(result).to be_nil - end - end -end diff --git a/spec/services/idv/steps/ipp/verify_step_spec.rb b/spec/services/idv/steps/ipp/verify_step_spec.rb index 07fd28e5f04..bfc949757b5 100644 --- a/spec/services/idv/steps/ipp/verify_step_spec.rb +++ b/spec/services/idv/steps/ipp/verify_step_spec.rb @@ -8,6 +8,7 @@ 'controller', session: { sp: { issuer: service_provider.issuer } }, current_user: user, + url_options: {}, ) end @@ -21,8 +22,7 @@ describe '#call' do it 'works' do - result = step.call - expect(result).to be_nil + step.call end end end diff --git a/spec/services/throttle_spec.rb b/spec/services/throttle_spec.rb index 66ff794ad2a..e45965d0f13 100644 --- a/spec/services/throttle_spec.rb +++ b/spec/services/throttle_spec.rb @@ -14,211 +14,194 @@ ) end - shared_examples 'throttle' do - describe '.new' do - context 'when target is a string' do - subject(:for_target) { Throttle.new(target: target, throttle_type: throttle_type) } - - context 'target is not a string' do - it 'raises an error' do - expect { Throttle.new(target: 3, throttle_type: throttle_type) }. - to raise_error(ArgumentError) - end + describe '.new' do + context 'when target is a string' do + subject(:for_target) { Throttle.new(target: target, throttle_type: throttle_type) } + + context 'target is not a string' do + it 'raises an error' do + expect { Throttle.new(target: 3, throttle_type: throttle_type) }. + to raise_error(ArgumentError) end end + end - it 'throws an error when neither user nor target are provided' do - expect { Throttle.new(throttle_type: throttle_type) }. - to raise_error( - ArgumentError, - 'Throttle must have a user or a target, but neither were provided', - ) - end + it 'throws an error when neither user nor target are provided' do + expect { Throttle.new(throttle_type: throttle_type) }. + to raise_error( + ArgumentError, + 'Throttle must have a user or a target, but neither were provided', + ) + end - it 'throws an error when both user and target are provided' do - expect { Throttle.new(throttle_type: throttle_type) }. - to raise_error( - ArgumentError, - 'Throttle must have a user or a target, but neither were provided', - ) - end + it 'throws an error when both user and target are provided' do + expect { Throttle.new(throttle_type: throttle_type) }. + to raise_error( + ArgumentError, + 'Throttle must have a user or a target, but neither were provided', + ) + end - it 'throws an error for an invalid throttle_type' do - expect { Throttle.new(throttle_type: :abc_123, target: '1') }. - to raise_error( - ArgumentError, - 'throttle_type is not valid', - ) - end + it 'throws an error for an invalid throttle_type' do + expect { Throttle.new(throttle_type: :abc_123, target: '1') }. + to raise_error( + ArgumentError, + 'throttle_type is not valid', + ) end + end - describe '.attempt_window_in_minutes' do - it 'returns configured attempt window for throttle type' do - expect(Throttle.attempt_window_in_minutes(throttle_type)).to eq(attempt_window) - end + describe '.attempt_window_in_minutes' do + it 'returns configured attempt window for throttle type' do + expect(Throttle.attempt_window_in_minutes(throttle_type)).to eq(attempt_window) + end - it 'is indifferent to throttle type stringiness' do - expect(Throttle.attempt_window_in_minutes(throttle_type.to_s)).to eq(attempt_window) - end + it 'is indifferent to throttle type stringiness' do + expect(Throttle.attempt_window_in_minutes(throttle_type.to_s)).to eq(attempt_window) end + end - describe '.max_attempts' do - it 'returns configured attempt window for throttle type' do - expect(Throttle.max_attempts(throttle_type)).to eq(max_attempts) - end + describe '.max_attempts' do + it 'returns configured attempt window for throttle type' do + expect(Throttle.max_attempts(throttle_type)).to eq(max_attempts) + end - it 'is indifferent to throttle type stringiness' do - expect(Throttle.max_attempts(throttle_type.to_s)).to eq(max_attempts) - end + it 'is indifferent to throttle type stringiness' do + expect(Throttle.max_attempts(throttle_type.to_s)).to eq(max_attempts) end + end - describe '#increment!' do - subject(:throttle) { Throttle.new(target: 'aaa', throttle_type: :idv_doc_auth) } + describe '#increment!' do + subject(:throttle) { Throttle.new(target: 'aaa', throttle_type: :idv_doc_auth) } - it 'increments db and redis attempts' do - expect(throttle.attempts).to eq 0 - throttle.increment! - expect(throttle.attempts).to eq 1 - expect(throttle.postgres_throttle.attempts).to eq 1 - end + it 'increments attempts' do + expect(throttle.attempts).to eq 0 + throttle.increment! + expect(throttle.attempts).to eq 1 end + end - describe '#throttled?' do - let(:throttle_type) { :idv_doc_auth } - let(:max_attempts) { IdentityConfig.store.doc_auth_max_attempts } - let(:attempt_window_in_minutes) { IdentityConfig.store.doc_auth_attempt_window_in_minutes } - - subject(:throttle) { Throttle.new(target: '1', throttle_type: throttle_type) } + describe '#throttled?' do + let(:throttle_type) { :idv_doc_auth } + let(:max_attempts) { IdentityConfig.store.doc_auth_max_attempts } + let(:attempt_window_in_minutes) { IdentityConfig.store.doc_auth_attempt_window_in_minutes } - it 'returns true if throttled' do - max_attempts.times do - throttle.increment! - end + subject(:throttle) { Throttle.new(target: '1', throttle_type: throttle_type) } - expect(throttle.throttled?).to eq(true) + it 'returns true if throttled' do + max_attempts.times do + throttle.increment! end - it 'returns false if the attempts < max_attempts' do - (max_attempts - 1).times do - expect(throttle.throttled?).to eq(false) - throttle.increment! - end + expect(throttle.throttled?).to eq(true) + end + it 'returns false if the attempts < max_attempts' do + (max_attempts - 1).times do expect(throttle.throttled?).to eq(false) + throttle.increment! end - it 'returns false if the attempts <= max_attempts but the window is expired' do - max_attempts.times do - throttle.increment! - end + expect(throttle.throttled?).to eq(false) + end - travel(attempt_window_in_minutes.minutes) do - expect(throttle.throttled?).to eq(false) - end + it 'returns false if the attempts <= max_attempts but the window is expired' do + max_attempts.times do + throttle.increment! + end + + travel(attempt_window_in_minutes.minutes) do + expect(throttle.throttled?).to eq(false) end end + end - describe '#throttled_else_increment?' do - subject(:throttle) { Throttle.new(target: 'aaaa', throttle_type: :idv_doc_auth) } + describe '#throttled_else_increment?' do + subject(:throttle) { Throttle.new(target: 'aaaa', throttle_type: :idv_doc_auth) } - context 'throttle has hit limit' do - before do - (IdentityConfig.store.doc_auth_max_attempts + 1).times do - throttle.increment! - end + context 'throttle has hit limit' do + before do + (IdentityConfig.store.doc_auth_max_attempts + 1).times do + throttle.increment! end + end - it 'is true' do - expect(throttle.throttled_else_increment?).to eq(true) - end + it 'is true' do + expect(throttle.throttled_else_increment?).to eq(true) end + end - context 'throttle has not hit limit' do - it 'is false' do - expect(throttle.throttled_else_increment?).to eq(false) - end + context 'throttle has not hit limit' do + it 'is false' do + expect(throttle.throttled_else_increment?).to eq(false) + end - it 'increments the throttle' do - expect { throttle.throttled_else_increment? }.to change { throttle.attempts }.by(1) - end + it 'increments the throttle' do + expect { throttle.throttled_else_increment? }.to change { throttle.attempts }.by(1) end end + end - describe '#expires_at' do - let(:attempted_at) { nil } - let(:throttle) { Throttle.new(target: '1', throttle_type: throttle_type) } + describe '#expires_at' do + let(:attempted_at) { nil } + let(:throttle) { Throttle.new(target: '1', throttle_type: throttle_type) } - context 'without having attempted' do - it 'returns current time' do - freeze_time do - expect(throttle.expires_at).to eq(Time.zone.now) - end + context 'without having attempted' do + it 'returns current time' do + freeze_time do + expect(throttle.expires_at).to eq(Time.zone.now) end end + end - context 'with expired throttle' do - it 'returns expiration time' do - throttle.increment! + context 'with expired throttle' do + it 'returns expiration time' do + throttle.increment! - travel_to(throttle.attempted_at + 3.days) do - expect(throttle.expires_at).to be_within(1.second). - of(throttle.attempted_at + attempt_window.minutes) - end + travel_to(throttle.attempted_at + 3.days) do + expect(throttle.expires_at).to be_within(1.second). + of(throttle.attempted_at + attempt_window.minutes) end end + end - context 'with active throttle' do - it 'returns expiration time' do - freeze_time do - throttle.increment! - expect(throttle.expires_at).to be_within(1.second). - of(throttle.attempted_at + attempt_window.minutes) - end + context 'with active throttle' do + it 'returns expiration time' do + freeze_time do + throttle.increment! + expect(throttle.expires_at).to be_within(1.second). + of(throttle.attempted_at + attempt_window.minutes) end end end + end - describe '#reset' do - let(:target) { '1' } - let(:subject) { described_class } + describe '#reset' do + let(:target) { '1' } + let(:subject) { described_class } - subject(:throttle) { Throttle.new(target: target, throttle_type: throttle_type) } + subject(:throttle) { Throttle.new(target: target, throttle_type: throttle_type) } - it 'resets attempt count to 0' do - throttle.increment! + it 'resets attempt count to 0' do + throttle.increment! - expect { throttle.reset! }.to change { throttle.attempts }.to(0) - end + expect { throttle.reset! }.to change { throttle.attempts }.to(0) end + end - describe '#remaining_count' do - let(:target) { '1' } - let(:subject) { described_class } - - subject(:throttle) { Throttle.new(target: target, throttle_type: throttle_type) } + describe '#remaining_count' do + let(:target) { '1' } + let(:subject) { described_class } - it 'returns maximium remaining attempts with zero attempts' do - expect(throttle.remaining_count).to eq(Throttle.max_attempts(throttle_type)) - end + subject(:throttle) { Throttle.new(target: target, throttle_type: throttle_type) } - it 'returns zero when throttle limit is reached' do - throttle.increment_to_throttled! - expect(throttle.remaining_count).to eq(0) - end + it 'returns maximium remaining attempts with zero attempts' do + expect(throttle.remaining_count).to eq(Throttle.max_attempts(throttle_type)) end - end - - context 'using Redis as throttle data store' do - before do - allow(IdentityConfig.store).to receive(:redis_throttle_enabled).and_return(true) - end - it_behaves_like 'throttle' - end - context 'using Postgres as throttle data store' do - before do - allow(IdentityConfig.store).to receive(:redis_throttle_enabled).and_return(false) + it 'returns zero when throttle limit is reached' do + throttle.increment_to_throttled! + expect(throttle.remaining_count).to eq(0) end - it_behaves_like 'throttle' end end diff --git a/spec/support/fake_analytics.rb b/spec/support/fake_analytics.rb index 8a327c5c5ce..cdfce6a6f6c 100644 --- a/spec/support/fake_analytics.rb +++ b/spec/support/fake_analytics.rb @@ -20,7 +20,7 @@ def track_event(event, original_attributes = {}) ERROR end - Idp::Constants::DEFAULT_MOCK_PII_FROM_DOC.slice( + Idp::Constants::MOCK_IDV_APPLICANT.slice( :first_name, :last_name, :address1, diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb index 66decf7074a..683f11acadb 100644 --- a/spec/support/features/doc_auth_helper.rb +++ b/spec/support/features/doc_auth_helper.rb @@ -3,7 +3,7 @@ module DocAuthHelper include DocumentCaptureStepHelper - GOOD_SSN = '900-66-1234' + GOOD_SSN = Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn] def session_from_completed_flow_steps(finished_step) session = { doc_auth: {} } @@ -185,7 +185,7 @@ def complete_proofing_steps end def mock_doc_auth_no_name_pii(method) - pii_with_no_name = Idp::Constants::DEFAULT_MOCK_PII_FROM_DOC.dup + pii_with_no_name = Idp::Constants::MOCK_IDV_APPLICANT.dup pii_with_no_name[:last_name] = nil DocAuth::Mock::DocAuthMockClient.mock_response!( method: method, diff --git a/spec/support/features/idv_step_helper.rb b/spec/support/features/idv_step_helper.rb index 8116e513c14..d2cd8f98a2c 100644 --- a/spec/support/features/idv_step_helper.rb +++ b/spec/support/features/idv_step_helper.rb @@ -43,8 +43,20 @@ def complete_idv_steps_before_phone_otp_verification_step(user = user_with_2fa) end def complete_idv_steps_with_phone_before_review_step(user = user_with_2fa) - complete_idv_steps_before_phone_step(user) - complete_phone_step(user) + if IdentityConfig.store.idv_api_enabled_steps.include?('password_confirm') + sign_in_and_2fa_user(user) + stub_idv_session( + applicant: Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE, + resolution_successful: true, + address_verification_mechanism: 'phone', + user_phone_confirmation: true, + vendor_phone_confirmation: true, + ) + visit idv_app_path(step: :password_confirm) + else + complete_idv_steps_before_phone_step(user) + complete_phone_step(user) + end end def complete_review_step(user) @@ -64,8 +76,18 @@ def complete_idv_steps_with_phone_before_confirmation_step(user = user_with_2fa) # rubocop:enable Layout/LineLength def complete_idv_steps_with_gpo_before_review_step(user = user_with_2fa) - complete_idv_steps_before_gpo_step(user) - click_on t('idv.buttons.mail.send') + if IdentityConfig.store.idv_api_enabled_steps.include?('password_confirm') + sign_in_and_2fa_user(user) + stub_idv_session( + applicant: Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN, + resolution_successful: 'phone', + address_verification_mechanism: 'gpo', + ) + visit idv_app_path(step: :password_confirm) + else + complete_idv_steps_before_gpo_step(user) + click_on t('idv.buttons.mail.send') + end end def complete_idv_steps_with_gpo_before_confirmation_step(user = user_with_2fa) @@ -78,4 +100,14 @@ def complete_idv_steps_with_gpo_before_confirmation_step(user = user_with_2fa) def complete_idv_steps_before_step(step, user = user_with_2fa) send("complete_idv_steps_before_#{step}_step", user) end + + private + + def stub_idv_session(**session_attributes) + allow(Idv::Session).to receive(:new).and_wrap_original do |original, kwargs| + result = original.call(**kwargs) + kwargs[:user_session][:idv].merge!(session_attributes) + result + end + end end diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index 34ffe9b3e77..34fc441520c 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -227,7 +227,7 @@ def user_with_piv_cac end def confirm_last_user - @raw_confirmation_token, = Devise.token_generator.generate(User, :confirmation_token) + @raw_confirmation_token, = Devise.token_generator.generate(EmailAddress, :confirmation_token) User.last.email_addresses.first.update( confirmation_token: @raw_confirmation_token, confirmation_sent_at: Time.zone.now, @@ -549,7 +549,7 @@ def set_up_2fa_with_authenticator_app def register_user_with_piv_cac(email = 'test@test.com') confirm_email_and_password(email) - expect(page).to have_current_path two_factor_options_path + expect(page).to have_current_path authentication_methods_setup_path expect(page).to have_content( t('two_factor_authentication.two_factor_choice_options.piv_cac'), ) diff --git a/spec/support/idv_examples/cancel_at_idv_step.rb b/spec/support/idv_examples/cancel_at_idv_step.rb deleted file mode 100644 index 3f6f855b875..00000000000 --- a/spec/support/idv_examples/cancel_at_idv_step.rb +++ /dev/null @@ -1,93 +0,0 @@ -shared_examples 'cancel at idv step' do |step, sp| - include SamlAuthHelper - - let(:fake_analytics) { FakeAnalytics.new } - - before do - start_idv_from_sp(sp) - complete_idv_steps_before_step(step) - allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) - end - - it 'shows the user a cancellation message with the option to go back to the step' do - original_path = current_path - - click_link t('links.cancel') - - expect(page).to have_content(t('headings.cancellations.prompt')) - expect(current_path).to eq(idv_cancel_path) - expect(fake_analytics).to have_logged_event( - 'IdV: cancellation visited', - step: step.to_s, - ) - - click_on t('links.go_back') - - expect(current_path).to eq(original_path) - expect(fake_analytics).to have_logged_event( - 'IdV: cancellation go back', - step: step.to_s, - ) - end - - context 'with an sp', if: sp do - it 'shows the user a cancellation message with the option to cancel and reset idv' do - sp_name = 'Test SP' - allow_any_instance_of(ServiceProviderSessionDecorator).to receive(:sp_name). - and_return(sp_name) - - click_link t('links.cancel') - - expect(page).to have_content(t('headings.cancellations.prompt')) - expect(current_path).to eq(idv_cancel_path) - expect(fake_analytics).to have_logged_event('IdV: cancellation visited', step: step.to_s) - - click_on t('forms.buttons.cancel') - - expect(page).to have_content(t('headings.cancellations.confirmation', app_name: APP_NAME)) - expect(current_path).to eq(idv_cancel_path) - expect(fake_analytics).to have_logged_event( - 'IdV: cancellation confirmed', - step: step.to_s, - ) - - expect(page).to have_link( - "‹ #{t('links.back_to_sp', sp: sp_name)}", - href: return_to_sp_failure_to_proof_path(step: step, location: 'cancel'), - ) - - # After visiting /verify, expect to redirect to the jurisdiction step, - # the first step in the IdV flow - visit idv_path - expect(current_path).to eq(idv_doc_auth_step_path(step: :welcome)) - end - end - - context 'without an sp' do - it 'shows a cancellation message with option to cancel and reset idv', if: sp.nil? do - click_link t('links.cancel') - - expect(page).to have_content(t('headings.cancellations.prompt')) - expect(current_path).to eq(idv_cancel_path) - expect(fake_analytics).to have_logged_event('IdV: cancellation visited', step: step.to_s) - - click_on t('forms.buttons.cancel') - - expect(page).to have_content(t('headings.cancellations.confirmation', app_name: APP_NAME)) - expect(current_path).to eq(idv_cancel_path) - expect(page).to have_link( - "‹ #{t('links.back_to_sp', sp: t('links.my_account'))}", - href: account_url, - ) - expect(fake_analytics).to have_logged_event( - 'IdV: cancellation confirmed', - step: step.to_s, - ) - - # After visiting /verify, expect to redirect to the jurisdiction step, - # the first step in the IdV flow - visit idv_path - expect(current_path).to eq(idv_doc_auth_step_path(step: :welcome)) - end - end -end diff --git a/spec/support/idv_examples/confirmation_step.rb b/spec/support/idv_examples/confirmation_step.rb index 81436859616..1d355a231ba 100644 --- a/spec/support/idv_examples/confirmation_step.rb +++ b/spec/support/idv_examples/confirmation_step.rb @@ -21,7 +21,8 @@ end context 'user selected gpo verification' do - it 'shows step indicator progress with pending verify phone step' do + it 'shows status content for gpo verification progress' do + expect(page).to have_content(t('idv.messages.mail_sent')) expect(page).to have_css( '.step-indicator__step--current', text: t('step_indicator.flows.idv.secure_account'), @@ -40,7 +41,8 @@ complete_idv_steps_with_phone_before_confirmation_step end - it 'shows step indicator progress with complete verify phone step' do + it 'shows status content for phone verification progress' do + expect(page).to have_content(t('idv.messages.confirm')) expect(page).to have_css( '.step-indicator__step--current', text: t('step_indicator.flows.idv.secure_account'), @@ -49,6 +51,7 @@ '.step-indicator__step--complete', text: t('step_indicator.flows.idv.verify_phone_or_address'), ) + expect(page).not_to have_css('.step-indicator__step--pending') end it 'redirects to the completions page and then to the SP', if: sp.present? do @@ -71,15 +74,5 @@ expect(page).to have_content(t('headings.account.verified_account')) expect(page).to have_current_path(account_path) end - - context 'user selected gpo verification' do - it 'shows step indicator progress without pending verify step' do - expect(page).to have_css( - '.step-indicator__step--current', - text: t('step_indicator.flows.idv.secure_account'), - ) - expect(page).not_to have_css('.step-indicator__step--pending') - end - end end end diff --git a/spec/support/idv_examples/sp_requested_attributes.rb b/spec/support/idv_examples/sp_requested_attributes.rb index 629a93b7d0d..ffe899cc569 100644 --- a/spec/support/idv_examples/sp_requested_attributes.rb +++ b/spec/support/idv_examples/sp_requested_attributes.rb @@ -3,14 +3,8 @@ include IdvStepHelper let(:user) { user_with_2fa } - let(:good_ssn) { DocAuthHelper::GOOD_SSN } let(:profile) { create(:profile, :active, :verified, user: user, pii: saved_pii) } - let(:saved_pii) do - Idp::Constants::DEFAULT_MOCK_PII_FROM_DOC.merge( - ssn: good_ssn, - phone: '+1 (555) 555-1234', - ) - end + let(:saved_pii) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE } context 'visiting an SP for the first time' do it 'requires the user to verify the attributes submitted to the SP', js: true do @@ -41,7 +35,11 @@ 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: good_ssn, visible: :hidden + expect(page).to have_css( + '.masked-text__text', + text: DocAuthHelper::GOOD_SSN, + visible: :hidden, + ) end end end @@ -103,7 +101,7 @@ 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 '+15555551234' + 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 diff --git a/spec/support/shared_examples_for_personal_keys.rb b/spec/support/shared_examples_for_personal_keys.rb index 67c32ad0010..23fa93e881f 100644 --- a/spec/support/shared_examples_for_personal_keys.rb +++ b/spec/support/shared_examples_for_personal_keys.rb @@ -1,20 +1,17 @@ +require 'rbconfig' + shared_examples_for 'personal key page' do include PersonalKeyHelper include JavascriptDriverHelper - context 'informational text' do + describe 'confirmation modal' do before do click_continue if javascript_enabled? end - context 'modal content' do - it 'displays the modal title' do - expect(page).to have_content t('forms.personal_key.title') - end - - it 'displays the modal instructions' do - expect(page).to have_content t('forms.personal_key.instructions') - end + it 'displays modal content' do + expect(page).to have_content t('forms.personal_key.title') + expect(page).to have_content t('forms.personal_key.instructions') end end @@ -36,6 +33,14 @@ copied_text = page.evaluate_async_script('navigator.clipboard.readText().then(arguments[0])') expect(copied_text).to eq(scrape_personal_key) + + click_continue + mod = mac? ? :meta : :control + page.find(':focus').send_keys [mod, 'v'] + + path_before_submit = current_path + within('[role=dialog]') { click_on t('forms.buttons.continue') } + expect(current_path).not_to eq path_before_submit end it 'validates as case-insensitive, crockford-normalized, length-limited, dash-flexible' do @@ -61,4 +66,8 @@ expect(current_path).not_to eq path_before_submit end end + + def mac? + RbConfig::CONFIG['host_os'].match? 'darwin' + end end diff --git a/spec/views/phone_setup/index.html.erb_spec.rb b/spec/views/phone_setup/index.html.erb_spec.rb index c8fd92fc000..7f1f223a877 100644 --- a/spec/views/phone_setup/index.html.erb_spec.rb +++ b/spec/views/phone_setup/index.html.erb_spec.rb @@ -23,7 +23,7 @@ it 'renders a link to choose a different option' do expect(render).to have_link( t('two_factor_authentication.choose_another_option'), - href: two_factor_options_path, + href: authentication_methods_setup_path, ) end diff --git a/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb b/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb index 3f3bcf9ab25..0bce6af8bb4 100644 --- a/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb +++ b/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb @@ -27,7 +27,7 @@ expect(rendered).to have_link( t('two_factor_authentication.choose_another_option'), - href: two_factor_options_path, + href: authentication_methods_setup_path, ) end end diff --git a/spec/views/users/totp_setup/new.html.erb_spec.rb b/spec/views/users/totp_setup/new.html.erb_spec.rb index d0a669b075f..22220251bb1 100644 --- a/spec/views/users/totp_setup/new.html.erb_spec.rb +++ b/spec/views/users/totp_setup/new.html.erb_spec.rb @@ -71,7 +71,7 @@ expect(rendered).to have_link( t('two_factor_authentication.choose_another_option'), - href: two_factor_options_path, + href: authentication_methods_setup_path, ) end end diff --git a/yarn.lock b/yarn.lock index 1faf55c3436..e0ee148e1de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3791,10 +3791,10 @@ iconv-lite@0.6.3, iconv-lite@^0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -identity-style-guide@^6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/identity-style-guide/-/identity-style-guide-6.4.2.tgz#3efcc311132de24fbe37cada262d3f84c23e72fe" - integrity sha512-NUIFXpoKjVI+Pout3MPx9F0rZ1/dbiDomvecv/VOvZaebD0by2iWtJ6JGm00I5Mhg74G5ePCGjnJeVkD15L7AQ== +identity-style-guide@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/identity-style-guide/-/identity-style-guide-6.5.0.tgz#41c869c2540faa22f80ad89ae9e0c1753e8940df" + integrity sha512-P/ZsOodZn3XyX6LYgrtDKzWe447rPkTUxAYADIGAcl13wv7ghQZJjaL1g1PySzG5PAvxeeoyOJYfRW58NRMUCw== dependencies: domready "1.0.8" uswds "^2.13.3"