diff --git a/app/controllers/account_reset/pending_controller.rb b/app/controllers/account_reset/pending_controller.rb index 855a6e12452..d1a713ea934 100644 --- a/app/controllers/account_reset/pending_controller.rb +++ b/app/controllers/account_reset/pending_controller.rb @@ -1,7 +1,7 @@ module AccountReset class PendingController < ApplicationController include UserAuthenticator - include ActionView::Helpers::DateHelper + include AccountResetConcern before_action :authenticate_user before_action :confirm_account_reset_request_exists @@ -12,7 +12,7 @@ def show end def confirm - @account_reset_deletion_period_interval = account_reset_deletion_period_interval + @account_reset_deletion_period_interval = account_reset_deletion_period_interval(current_user) end def cancel @@ -32,16 +32,5 @@ def pending_account_reset_request current_user, ).call end - - def account_reset_deletion_period_interval - current_time = Time.zone.now - - distance_of_time_in_words( - current_time, - current_time + IdentityConfig.store.account_reset_wait_period_days.days, - true, - accumulate_on: :hours, - ) - end end end diff --git a/app/controllers/account_reset/request_controller.rb b/app/controllers/account_reset/request_controller.rb index afc55e4df98..7f51aea7740 100644 --- a/app/controllers/account_reset/request_controller.rb +++ b/app/controllers/account_reset/request_controller.rb @@ -1,13 +1,13 @@ module AccountReset class RequestController < ApplicationController include TwoFactorAuthenticatable - include ActionView::Helpers::DateHelper + include AccountResetConcern before_action :confirm_two_factor_enabled def show analytics.account_reset_visit - @account_reset_deletion_period_interval = account_reset_deletion_period_interval + @account_reset_deletion_period_interval = account_reset_deletion_period_interval(current_user) end def create @@ -41,16 +41,5 @@ def analytics_attributes email_addresses: current_user.email_addresses.count, } end - - def account_reset_deletion_period_interval - current_time = Time.zone.now - - distance_of_time_in_words( - current_time, - current_time + IdentityConfig.store.account_reset_wait_period_days.days, - true, - accumulate_on: :hours, - ) - end end end diff --git a/app/controllers/concerns/account_reset_concern.rb b/app/controllers/concerns/account_reset_concern.rb new file mode 100644 index 00000000000..15457c8fb87 --- /dev/null +++ b/app/controllers/concerns/account_reset_concern.rb @@ -0,0 +1,38 @@ +module AccountResetConcern + include ActionView::Helpers::DateHelper + def account_reset_deletion_period_interval(user) + current_time = Time.zone.now + + distance_of_time_in_words( + current_time, + current_time + account_reset_wait_period_days(user), + true, + accumulate_on: reset_accumulation_type(user), + ) + end + + def account_reset_wait_period_days(user) + if supports_fraud_account_reset?(user) + IdentityConfig.store.account_reset_fraud_user_wait_period_days.days + else + IdentityConfig.store.account_reset_wait_period_days.days + end + end + + def supports_fraud_account_reset?(user) + IdentityConfig.store.account_reset_fraud_user_wait_period_days.present? && + fraud_state?(user) + end + + def fraud_state?(user) + user.fraud_review_pending? || user.fraud_rejection? + end + + def reset_accumulation_type(user) + if account_reset_wait_period_days(user) > 3.days + :days + else + :hours + end + end +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 19ff2a46794..5027083feb7 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -15,6 +15,7 @@ class UserMailer < ActionMailer::Base include Mailable include LocaleHelper + include AccountResetConcern include ActionView::Helpers::DateHelper class UserEmailAddressMismatchError < StandardError; end @@ -144,10 +145,10 @@ def personal_key_regenerated def account_reset_request(account_reset) with_user_locale(user) do @token = account_reset&.request_token - @account_reset_deletion_period_hours = account_reset_deletion_period_hours + @account_reset_deletion_period_interval = account_reset_deletion_period_interval(user) @header = t( 'user_mailer.account_reset_request.header', - interval: account_reset_deletion_period_interval, + interval: @account_reset_deletion_period_interval, ) mail( to: email_address.email, @@ -160,7 +161,7 @@ def account_reset_granted(account_reset) with_user_locale(user) do @token = account_reset&.request_token @granted_token = account_reset&.granted_token - @account_reset_deletion_period_hours = account_reset_deletion_period_hours + @account_reset_deletion_period_interval = account_reset_deletion_period_interval(user) @account_reset_token_valid_period = account_reset_token_valid_period mail( to: email_address.email, @@ -431,21 +432,6 @@ def account_reinstated private - def account_reset_deletion_period_interval - current_time = Time.zone.now - - distance_of_time_in_words( - current_time, - current_time + IdentityConfig.store.account_reset_wait_period_days.days, - true, - accumulate_on: :hours, - ) - end - - def account_reset_deletion_period_hours - IdentityConfig.store.account_reset_wait_period_days.days.in_hours.to_i - end - def account_reset_token_valid_period current_time = Time.zone.now diff --git a/app/presenters/account_reset/pending_presenter.rb b/app/presenters/account_reset/pending_presenter.rb index 636b7bcd081..3325adec06d 100644 --- a/app/presenters/account_reset/pending_presenter.rb +++ b/app/presenters/account_reset/pending_presenter.rb @@ -1,5 +1,6 @@ module AccountReset class PendingPresenter + include AccountResetConcern include ActionView::Helpers::DateHelper attr_reader :account_reset_request @@ -9,7 +10,7 @@ def initialize(account_reset_request) end def time_remaining_until_granted(now: Time.zone.now) - wait_time = IdentityConfig.store.account_reset_wait_period_days.days + wait_time = account_reset_wait_period_days(user) distance_of_time_in_words( now, @@ -19,8 +20,12 @@ def time_remaining_until_granted(now: Time.zone.now) ) end - def account_reset_deletion_period_hours - IdentityConfig.store.account_reset_wait_period_days.days.in_hours.to_i + def account_reset_deletion_period + account_reset_deletion_period_interval(user) + end + + def user + account_reset_request.user end end end diff --git a/app/presenters/two_factor_login_options_presenter.rb b/app/presenters/two_factor_login_options_presenter.rb index 0d9556a4168..cff2c81bfb0 100644 --- a/app/presenters/two_factor_login_options_presenter.rb +++ b/app/presenters/two_factor_login_options_presenter.rb @@ -1,4 +1,5 @@ class TwoFactorLoginOptionsPresenter < TwoFactorAuthCode::GenericDeliveryPresenter + include AccountResetConcern include ActionView::Helpers::TranslationHelper attr_reader :user, :reauthentication_context, :phishing_resistant_required, :piv_cac_required @@ -117,7 +118,7 @@ def account_reset_cancel_link [ t( 'two_factor_authentication.account_reset.pending', - interval: account_reset_deletion_period_interval, + interval: account_reset_deletion_period_interval(user), ), @view.link_to( t('two_factor_authentication.account_reset.cancel_link'), @@ -143,15 +144,4 @@ def sp_name APP_NAME end end - - def account_reset_deletion_period_interval - current_time = Time.zone.now - - view.distance_of_time_in_words( - current_time, - current_time + IdentityConfig.store.account_reset_wait_period_days.days, - true, - accumulate_on: :hours, - ) - end end diff --git a/app/services/account_reset/create_request.rb b/app/services/account_reset/create_request.rb index bbe2d05de2b..61da2996239 100644 --- a/app/services/account_reset/create_request.rb +++ b/app/services/account_reset/create_request.rb @@ -1,5 +1,6 @@ module AccountReset class CreateRequest + include AccountResetConcern include ActionView::Helpers::DateHelper def initialize(user, requesting_issuer) @@ -48,23 +49,12 @@ def notify_user_by_sms_if_applicable @telephony_response = Telephony.send_account_reset_notice( to: phone, country_code: Phonelib.parse(phone).country, - interval: account_reset_wait_period, + interval: account_reset_deletion_period_interval(user), ) end def extra_analytics_attributes @telephony_response&.extra&.slice(:request_id, :message_id) || {} end - - def account_reset_wait_period - current_time = Time.zone.now - - distance_of_time_in_words( - current_time, - current_time + IdentityConfig.store.account_reset_wait_period_days, - true, - accumulate_on: :hours, - ) - end end end diff --git a/app/services/account_reset/find_pending_request_for_user.rb b/app/services/account_reset/find_pending_request_for_user.rb index e8d27db2f04..79ca655beba 100644 --- a/app/services/account_reset/find_pending_request_for_user.rb +++ b/app/services/account_reset/find_pending_request_for_user.rb @@ -1,5 +1,6 @@ module AccountReset class FindPendingRequestForUser + include AccountResetConcern attr_reader :user def initialize(user) @@ -13,7 +14,7 @@ def call cancelled_at: nil, ).where( 'requested_at > ?', - IdentityConfig.store.account_reset_wait_period_days.days.ago, + account_reset_wait_period_days(user).ago, ).order(requested_at: :asc).first end end diff --git a/app/services/account_reset/grant_request.rb b/app/services/account_reset/grant_request.rb index 0598458378b..0dec3f3d27f 100644 --- a/app/services/account_reset/grant_request.rb +++ b/app/services/account_reset/grant_request.rb @@ -7,9 +7,10 @@ def initialize(user) def call token = SecureRandom.uuid arr = AccountResetRequest.find_by(user_id: @user_id) + return false if fraud_user?(arr) && fraud_wait_period_not_met?(arr) result = arr.with_lock do if !arr.granted_token_valid? - account_reset_request.update( + arr.update( granted_at: Time.zone.now, granted_token: token, ) @@ -21,8 +22,21 @@ def call private - def account_reset_request - AccountResetRequest.create_or_find_by(user_id: @user_id) + def fraud_user?(arr) + arr.user.fraud_review_pending? || + arr.user.fraud_rejection? + end + + def fraud_wait_period_not_met?(arr) + if fraud_wait_period_days.present? + return arr.requested_at > (Time.zone.now - fraud_wait_period_days.days) + else + false + end + end + + def fraud_wait_period_days + IdentityConfig.store.account_reset_fraud_user_wait_period_days end end end diff --git a/app/views/account_reset/pending/show.html.erb b/app/views/account_reset/pending/show.html.erb index ceb730fe1fc..970283d4574 100644 --- a/app/views/account_reset/pending/show.html.erb +++ b/app/views/account_reset/pending/show.html.erb @@ -5,7 +5,7 @@

<%= t( 'account_reset.pending.wait_html', - hours: @pending_presenter.account_reset_deletion_period_hours, + waiting_period: @pending_presenter.account_reset_deletion_period, interval: @pending_presenter.time_remaining_until_granted, ) %>

diff --git a/app/views/user_mailer/account_reset_granted.html.erb b/app/views/user_mailer/account_reset_granted.html.erb index 70d608be1d9..4da0fcc5ec8 100644 --- a/app/views/user_mailer/account_reset_granted.html.erb +++ b/app/views/user_mailer/account_reset_granted.html.erb @@ -1,5 +1,5 @@

- <%= t('user_mailer.account_reset_granted.intro_html', hours: @account_reset_deletion_period_hours, app_name: link_to(APP_NAME, IdentityConfig.store.mailer_domain_name, class: 'gray')) %> + <%= t('user_mailer.account_reset_granted.intro_html', waiting_period: @account_reset_deletion_period_interval, app_name: link_to(APP_NAME, IdentityConfig.store.mailer_domain_name, class: 'gray')) %>

diff --git a/app/views/user_mailer/account_reset_request.html.erb b/app/views/user_mailer/account_reset_request.html.erb index d486d654efc..932fcfad3a5 100644 --- a/app/views/user_mailer/account_reset_request.html.erb +++ b/app/views/user_mailer/account_reset_request.html.erb @@ -1,5 +1,5 @@

- <%= t('user_mailer.account_reset_request.intro_html', app_name: link_to(APP_NAME, IdentityConfig.store.mailer_domain_name, class: 'gray'), hours: @account_reset_deletion_period_hours) %> + <%= t('user_mailer.account_reset_request.intro_html', app_name: link_to(APP_NAME, IdentityConfig.store.mailer_domain_name, class: 'gray'), waiting_period: @account_reset_deletion_period_interval) %>

diff --git a/config/application.yml.default b/config/application.yml.default index 865bba115a4..b188461db5c 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -24,6 +24,7 @@ all_redirect_uris_cache_duration_minutes: 2 allowed_ialmax_providers: '[]' allowed_verified_within_providers: '[]' account_reset_token_valid_for_days: 1 +account_reset_fraud_user_wait_period_days: account_reset_wait_period_days: 1 account_suspended_support_code: EFGHI acuant_assure_id_password: '' @@ -509,6 +510,7 @@ production: test: aamva_private_key: 123abc aamva_public_key: 123abc + account_reset_fraud_user_wait_period_days: 30 acuant_assure_id_url: https://example.com acuant_facial_match_url: https://facial_match.example.com acuant_passlive_url: https://liveness.example.com diff --git a/config/locales/account_reset/en.yml b/config/locales/account_reset/en.yml index f3fe9cf0d60..ea9493777fd 100644 --- a/config/locales/account_reset/en.yml +++ b/config/locales/account_reset/en.yml @@ -33,8 +33,8 @@ en: confirm: If you cancel now, you must create a new request and wait another %{interval} to delete your account. header: You requested to delete your account - wait_html: There is a %{hours}-hour waiting period to delete your account. In - %{interval}, you will receive an email with + wait_html: There is a waiting period of %{waiting_period} to delete your + account. In %{interval}, you will receive an email with instructions to complete the deletion. recovery_options: check_saved_credential: See if you have a saved credential diff --git a/config/locales/account_reset/es.yml b/config/locales/account_reset/es.yml index 9f8f1dc4317..3cd6eb93a1d 100644 --- a/config/locales/account_reset/es.yml +++ b/config/locales/account_reset/es.yml @@ -34,9 +34,9 @@ es: confirm: Si cancela ahora, debe crear una nueva solicitud y esperar otras %{interval} para eliminar su cuenta. header: Solicitaste eliminar tu cuenta - wait_html: Hay un período de espera de %{hours} horas para eliminar su cuenta. - En %{interval}, recibirá un correo electrónico con - instrucciones para completar la eliminación. + wait_html: Hay un período de espera de %{waiting_period} para eliminar su + cuenta. En %{interval}, recibirá un correo electrónico + con instrucciones para completar la eliminación. recovery_options: check_saved_credential: Verifica si tienes una credencial almacenada check_webauthn_platform_info: Si has habilitado el desbloqueo facial o táctil, diff --git a/config/locales/account_reset/fr.yml b/config/locales/account_reset/fr.yml index cd52a50fa08..f26afe7d5e0 100644 --- a/config/locales/account_reset/fr.yml +++ b/config/locales/account_reset/fr.yml @@ -34,7 +34,7 @@ fr: confirm: Si vous annulez maintenant, vous devez créer une nouvelle demande et attendre encore %{interval} pour supprimer votre compte. header: Vous avez demandé de supprimer votre compte - wait_html: Il y a un délai d’attente de %{hours} heures pour supprimer votre + wait_html: Il y a un délai d’attente de %{waiting_period} pour supprimer votre compte. Dans %{interval}, vous recevrez un e-mail avec des instructions pour terminer la suppression. recovery_options: diff --git a/config/locales/user_mailer/en.yml b/config/locales/user_mailer/en.yml index 430d6552c71..16585668655 100644 --- a/config/locales/user_mailer/en.yml +++ b/config/locales/user_mailer/en.yml @@ -20,8 +20,8 @@ en: button: Yes, continue deleting cancel_link_text: please cancel help_html: If you don’t want to delete your account, %{cancel_account_reset_html}. - intro_html: Your %{hours} hour waiting period has ended. Please complete step 2 - of the process.

If you’ve been unable to locate your + intro_html: Your waiting period of %{waiting_period} has ended. Please complete + step 2 of the process.

If you’ve been unable to locate your authentication methods, select “confirm deletion” to delete your %{app_name} account.

In the future, if you need to access participating government websites who use %{app_name}, you can create a @@ -33,13 +33,14 @@ en: to cancel. header: Your account will be deleted in %{interval} intro_html: 'As a security measure, %{app_name} requires a two-step process to - delete your account:

Step One: There is a %{hours} hour waiting - period if you have lost access to your authentication methods and need - to delete your account. If you locate your authentication methods, you - can sign in to your %{app_name} account to cancel this request.

- Step Two: After your %{hours} hour waiting period, you will receive an - email that will ask you to confirm the deletion of your %{app_name} - account. Your account will not be deleted until you confirm.' + delete your account:

Step One: There is a waiting period of + %{waiting_period} if you have lost access to your authentication methods + and need to delete your account. If you locate your authentication + methods, you can sign in to your %{app_name} account to cancel this + request.

Step Two: After the waiting period of + %{waiting_period}, you will receive an email that will ask you to + confirm the deletion of your %{app_name} account. Your account will not + be deleted until you confirm.' subject: How to delete your %{app_name} account account_verified: change_password_link: change your password diff --git a/config/locales/user_mailer/es.yml b/config/locales/user_mailer/es.yml index e71f33c9287..b7c372edafb 100644 --- a/config/locales/user_mailer/es.yml +++ b/config/locales/user_mailer/es.yml @@ -22,8 +22,8 @@ es: button: Sí, continúa eliminando cancel_link_text: por favor cancele help_html: Si no desea eliminar su cuenta, %{cancel_account_reset_html}. - intro_html: Su período de espera de %{hours} horas ha finalizado. Complete el - paso 2 del proceso.

Si no ha podido localizar sus métodos de + intro_html: Su período de espera de %{waiting_period} finalizó. Complete el paso + 2 del proceso.

Si no ha podido localizar sus métodos de autenticación, seleccione “confirmar eliminación” para eliminar su cuenta de %{app_name}.

En el futuro, si necesita acceder a los sitios web gubernamentales participantes que utilizan %{app_name}, puede @@ -35,14 +35,14 @@ es: para cancelar.' header: Su cuenta será eliminada en %{interval} intro_html: 'Como medida de seguridad, %{app_name} requiere un proceso de dos - pasos para eliminar su cuenta:

Paso uno: hay un período de - espera de %{hours} horas si ha perdido el acceso a sus métodos de + pasos para eliminar su cuenta:

Paso uno: Hay un período de + espera de %{waiting_period} si perdió el acceso a sus métodos de autenticación y necesita eliminar su cuenta. Si encuentra sus métodos de autenticación, puede iniciar sesión en su cuenta %{app_name} para - cancelar esta solicitud.

Paso dos: Después de su período de - espera de %{hours} horas, recibirá un correo electrónico que le pedirá - que confirme la eliminación de su cuenta %{app_name}. Su cuenta no se - eliminará hasta que confirme.' + cancelar esta solicitud.

Paso dos: Tras el período de espera de + %{waiting_period}, recibirás un correo electrónico en el que te + pediremos que confirmes la eliminación de tu cuenta %{app_name}. Tu + cuenta no se eliminará hasta que lo confirmes.' subject: Cómo eliminar su cuenta de %{app_name} account_verified: change_password_link: cambiar tu contraseña diff --git a/config/locales/user_mailer/fr.yml b/config/locales/user_mailer/fr.yml index cf220fa3213..b52f7806e2d 100644 --- a/config/locales/user_mailer/fr.yml +++ b/config/locales/user_mailer/fr.yml @@ -22,7 +22,7 @@ fr: cancel_link_text: veuillez annuler help_html: Si vous ne souhaitez pas supprimer votre compte, %{cancel_account_reset_html}. - intro_html: Votre période d’attente de %{hours} heures est terminée. Veuillez + intro_html: Votre délai d’attente de %{waiting_period} est terminé. Veuillez terminer l’étape 2 du processus.

Si vous ne parvenez pas à localiser vos méthodes d’authentification, sélectionnez “confirmer la suppression” pour supprimer votre compte %{app_name}.

À @@ -36,15 +36,14 @@ fr: %{app_name} pour annuler. header: Votre compte sera supprimé dans %{interval} intro_html: 'Par mesure de sécurité, %{app_name} nécessite un processus en deux - étapes pour supprimer votre compte:

Étape 1: Il y a une - période d’attente de %{hours} heures si vous avez perdu l’accès à vos - méthodes d’authentification et devez supprimer votre compte. Si vous - trouvez vos méthodes d’authentification, vous pouvez vous connecter à - votre compte %{app_name} pour annuler cette demande.

Deuxième - étape: après votre période d’attente de %{hours} heures, vous recevrez - un e-mail qui vous demandera de confirmer la suppression de votre compte - %{app_name}. Votre compte ne sera pas supprimé tant que vous ne l’aurez - pas confirmé.' + étapes pour supprimer votre compte:

Étape 1: Il y a un delai + d’attente de %{waiting_period} si vous avez perdu l’accès à vos méthodes + d’authentification et devez supprimer votre compte. Si vous trouvez vos + méthodes d’authentification, vous pouvez vous connecter à votre compte + %{app_name} pour annuler cette demande.

Deuxième étape: après + la période d’attente de %{waiting_period}, vous recevrez un e-mail qui + vous demandera de confirmer la suppression de votre compte %{app_name}. + Votre compte ne sera pas supprimé tant que vous n’aurez pas confirmé.' subject: Comment supprimer votre compte %{app_name} account_verified: change_password_link: changer votre mot de passe diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 45029c9bd20..40436cf5024 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -108,6 +108,7 @@ def self.build_store(config_map) config.add(:aamva_verification_url) config.add(:account_reset_token_valid_for_days, type: :integer) config.add(:account_reset_wait_period_days, type: :integer) + config.add(:account_reset_fraud_user_wait_period_days, type: :integer, allow_nil: true) config.add(:account_suspended_support_code, type: :string) config.add(:acuant_assure_id_password) config.add(:acuant_assure_id_subscription_id) diff --git a/spec/controllers/account_reset/pending_controller_spec.rb b/spec/controllers/account_reset/pending_controller_spec.rb index 3348bb4cf70..3155f6f56a2 100644 --- a/spec/controllers/account_reset/pending_controller_spec.rb +++ b/spec/controllers/account_reset/pending_controller_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' RSpec.describe AccountReset::PendingController do + include ActionView::Helpers::DateHelper + include AccountResetHelper let(:user) { create(:user) } before do @@ -17,6 +19,67 @@ end end + describe '#confirm' do + before do + end + context 'non-fraud user' do + it 'should have @account_reset_deletion_period_interval to match regular wait period' do + create_account_reset_request_for(user) + + get :confirm + current_time = Time.zone.now + time_in_hours = distance_of_time_in_words( + current_time, + current_time + IdentityConfig.store.account_reset_wait_period_days.days, + true, + accumulate_on: :hours, + ) + expect(controller.view_assigns['account_reset_deletion_period_interval']). + to eq(time_in_hours) + end + end + + context 'fraud user' do + let(:user) { create(:user, :fraud_review_pending) } + context 'fraud wait period not set' do + before do + allow(IdentityConfig.store).to receive(:account_reset_fraud_user_wait_period_days). + and_return(nil) + end + + it 'should have @account_reset_deletion_period to match regular wait period' do + create_account_reset_request_for(user) + + get :confirm + current_time = Time.zone.now + time_in_hours = distance_of_time_in_words( + current_time, + current_time + IdentityConfig.store.account_reset_wait_period_days.days, + true, + accumulate_on: :hours, + ) + expect(controller.view_assigns['account_reset_deletion_period_interval']). + to eq(time_in_hours) + end + end + + it 'should have @account_reset_deletion_period_interval to match fraud wait period' do + create_account_reset_request_for(user) + + get :confirm + current_time = Time.zone.now + time_in_hours = distance_of_time_in_words( + current_time, + current_time + IdentityConfig.store.account_reset_fraud_user_wait_period_days.days, + true, + accumulate_on: :days, + ) + expect(controller.view_assigns['account_reset_deletion_period_interval']). + to eq(time_in_hours) + end + end + end + describe '#cancel' do it 'cancels the account reset request and logs the cancellation event' do stub_attempts_tracker diff --git a/spec/controllers/account_reset/request_controller_spec.rb b/spec/controllers/account_reset/request_controller_spec.rb index 09c7484d905..5077481222c 100644 --- a/spec/controllers/account_reset/request_controller_spec.rb +++ b/spec/controllers/account_reset/request_controller_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' RSpec.describe AccountReset::RequestController, allowed_extra_analytics: [:*] do + include ActionView::Helpers::DateHelper let(:user) { create(:user, :with_authentication_app) } describe '#show' do it 'renders the page' do @@ -32,6 +33,64 @@ get :show end + + context 'non-fraud user' do + it 'should have @account_reset_deletion_period_interval to match regular wait period' do + stub_sign_in_before_2fa(user) + + get :show + current_time = Time.zone.now + time_in_hours = distance_of_time_in_words( + current_time, + current_time + IdentityConfig.store.account_reset_wait_period_days.days, + true, + accumulate_on: :hours, + ) + expect(controller.view_assigns['account_reset_deletion_period_interval']). + to eq(time_in_hours) + end + end + + context 'fraud user' do + let(:user) { create(:user, :fraud_review_pending) } + + context 'fraud wait period not set' do + before do + allow(IdentityConfig.store).to receive(:account_reset_fraud_user_wait_period_days). + and_return(nil) + end + + it 'should have @account_reset_deletion_period to match regular wait period' do + stub_sign_in_before_2fa(user) + + get :show + current_time = Time.zone.now + time_in_hours = distance_of_time_in_words( + current_time, + current_time + IdentityConfig.store.account_reset_wait_period_days.days, + true, + accumulate_on: :hours, + ) + expect(controller.view_assigns['account_reset_deletion_period_interval']). + to eq(time_in_hours) + end + end + + it 'should have @account_reset_deletion_period_interval to match fraud wait period' do + stub_sign_in_before_2fa(user) + + get :show + current_time = Time.zone.now + time_in_hours = distance_of_time_in_words( + current_time, + current_time + IdentityConfig.store.account_reset_fraud_user_wait_period_days.days, + true, + accumulate_on: :days, + ) + expect(controller.view_assigns['account_reset_deletion_period_interval']). + to eq(time_in_hours) + end + end end describe '#create' do diff --git a/spec/controllers/concerns/account_reset_concern_spec.rb b/spec/controllers/concerns/account_reset_concern_spec.rb new file mode 100644 index 00000000000..f19a2c97ad1 --- /dev/null +++ b/spec/controllers/concerns/account_reset_concern_spec.rb @@ -0,0 +1,68 @@ +require 'rails_helper' + +RSpec.describe AccountResetConcern do + include ActionView::Helpers::DateHelper + let(:test_class) do + Class.new do + include AccountResetConcern + + attr_reader :current_user + + def initialize(current_user:) + @current_user = current_user + end + end + end + let(:user) { build(:user) } + let(:instance) { test_class.new(current_user: user) } + + describe '#account_reset_deletion_period_interval' do + context 'non fraud user' do + it 'should return regular wait time' do + current_time = Time.zone.now + time_in_hours = distance_of_time_in_words( + current_time, + current_time + IdentityConfig.store.account_reset_wait_period_days.days, + true, + accumulate_on: :hours, + ) + expect(instance.account_reset_deletion_period_interval(user)). + to eq(time_in_hours) + end + end + + context 'fraud user' do + let(:user) { create(:user, :fraud_review_pending) } + it 'should return fraud wait time' do + current_time = Time.zone.now + time_in_hours = distance_of_time_in_words( + current_time, + current_time + IdentityConfig.store.account_reset_fraud_user_wait_period_days.days, + true, + accumulate_on: :days, + ) + expect(instance.account_reset_deletion_period_interval(user)). + to eq(time_in_hours) + end + + context 'when account_reset_fraud_user_wait_period_days is nil' do + before do + allow(IdentityConfig.store).to receive(:account_reset_fraud_user_wait_period_days). + and_return(nil) + end + + it 'should return standard reset wait time' do + current_time = Time.zone.now + time_in_hours = distance_of_time_in_words( + current_time, + current_time + IdentityConfig.store.account_reset_wait_period_days.days, + true, + accumulate_on: :hours, + ) + expect(instance.account_reset_deletion_period_interval(user)). + to eq(time_in_hours) + end + end + end + end +end diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 20d86af472f..aaac96febaf 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -304,7 +304,7 @@ def expect_email_body_to_have_help_and_contact_links let(:account_reset) { user.account_reset_request } let(:interval) { '24 hours' } - let(:account_reset_deletion_period_hours) { 24 } + let(:account_reset_deletion_period_hours) { '24 hours' } it_behaves_like 'a system email' it_behaves_like 'an email that respects user email locale preference' @@ -323,7 +323,7 @@ def expect_email_body_to_have_help_and_contact_links t( 'user_mailer.account_reset_request.intro_html', app_name: APP_NAME, interval: interval, - hours: + waiting_period: account_reset_deletion_period_hours ), ), @@ -355,7 +355,7 @@ def expect_email_body_to_have_help_and_contact_links UserMailer.with(user: user, email_address: email_address). account_reset_granted(user.account_reset_request) end - let(:account_reset_deletion_period_hours) { 24 } + let(:account_reset_deletion_period_hours) { '24 hours' } let(:token_expiration_interval) { '24 hours' } it_behaves_like 'a system email' @@ -377,7 +377,7 @@ def expect_email_body_to_have_help_and_contact_links strip_tags( t( 'user_mailer.account_reset_granted.intro_html', app_name: APP_NAME, - hours: + waiting_period: account_reset_deletion_period_hours ), ), diff --git a/spec/presenters/account_reset/pending_presenter_spec.rb b/spec/presenters/account_reset/pending_presenter_spec.rb index b4bb776a0aa..631d6976dcc 100644 --- a/spec/presenters/account_reset/pending_presenter_spec.rb +++ b/spec/presenters/account_reset/pending_presenter_spec.rb @@ -20,6 +20,36 @@ describe '#time_remaining_until_granted' do before { I18n.locale = :en } + before do + allow(IdentityConfig.store).to receive(:account_reset_fraud_user_wait_period_days). + and_return(10) + end + + context 'fraud user' do + let(:user) { create(:user, :fraud_review_pending) } + context 'when the remaining time is greater than 1 week' do + let(:requested_at) { 10.days.ago - (9.days + 21.minutes) } + + it 'returns its description in week and days' do + expect(subject.time_remaining_until_granted).to eq '1 week and 2 days' + end + end + context 'when the remaining time is greater than 3 days and less than 1 weeks' do + let(:requested_at) { 10.days.ago - (5.days + 21.hours) } + + it 'returns its description in hours and minutes' do + expect(subject.time_remaining_until_granted).to eq '5 days and 21 hours' + end + end + + context 'when the remaining time is less than 1 day' do + let(:requested_at) { 10.days.ago - (10.hours + 21.minutes) } + + it 'returns its description in hours and minutes' do + expect(subject.time_remaining_until_granted).to eq '10 hours and 21 minutes' + end + end + end context 'when the remaining time is greater than 1 hour' do let(:requested_at) { 24.hours.ago - (2.hours + 21.minutes) } diff --git a/spec/services/account_reset/find_prending_request_for_user_spec.rb b/spec/services/account_reset/find_pending_request_for_user_spec.rb similarity index 70% rename from spec/services/account_reset/find_prending_request_for_user_spec.rb rename to spec/services/account_reset/find_pending_request_for_user_spec.rb index d2a5fb7ba38..bad22dc2f96 100644 --- a/spec/services/account_reset/find_prending_request_for_user_spec.rb +++ b/spec/services/account_reset/find_pending_request_for_user_spec.rb @@ -45,5 +45,20 @@ it { expect(subject.call).to be_nil } end + + context 'fraud user' do + let(:user) { create(:user, :fraud_review_pending) } + let(:user2) { create(:user, :fraud_rejection) } + context 'when a request exists, and it hasnt been granted yet but over a day' do + let(:requested_at) { 30.hours.ago } + + it { expect(subject.call).to eq(account_reset_request) } + end + context 'when a request has expired' do + let(:requested_at) { 1.year.ago } + + it { expect(subject.call).to be_nil } + end + end end end diff --git a/spec/services/account_reset/grant_request_spec.rb b/spec/services/account_reset/grant_request_spec.rb index be3fafbeda7..f87bd5803ea 100644 --- a/spec/services/account_reset/grant_request_spec.rb +++ b/spec/services/account_reset/grant_request_spec.rb @@ -29,5 +29,81 @@ expect(arr.granted_token).to eq(arr.reload.granted_token) end end + + context 'with a fraud user' do + let(:user) { create(:user, :fraud_review_pending) } + let(:user2) { create(:user, :fraud_rejection) } + context 'with nil being set for fraud time' do + before do + allow(IdentityConfig.store).to receive(:account_reset_fraud_user_wait_period_days). + and_return(nil) + end + + it 'grants request for all users' do + before_waiting_the_full_wait_period(Time.zone.now) do + create_account_reset_request_for(user) + create_account_reset_request_for(user2) + end + + result = AccountReset::GrantRequest.new(user).call + arr = AccountResetRequest.find_by(user_id: user.id) + expect(arr.granted_at).to be_present + expect(arr.granted_token).to be_present + expect(result).to eq true + + result2 = AccountReset::GrantRequest.new(user2).call + arr2 = AccountResetRequest.find_by(user_id: user2.id) + expect(arr2.granted_at).to be_present + expect(arr2.granted_token).to be_present + expect(result2).to eq true + end + end + context 'with deletion period not met' do + it 'does not grant request' do + create_account_reset_request_for(user) + result = AccountReset::GrantRequest.new(user).call + + arr = AccountResetRequest.find_by(user_id: user.id) + expect(arr.granted_at).to_not be_present + expect(arr.granted_token).to_not be_present + expect(result).to eq false + end + end + + context 'with deletion period met' do + it 'grants request for all users' do + before_waiting_the_full_fraud_wait_period(Time.zone.now) do + create_account_reset_request_for(user) + create_account_reset_request_for(user2) + end + + result = AccountReset::GrantRequest.new(user).call + arr = AccountResetRequest.find_by(user_id: user.id) + expect(arr.granted_at).to be_present + expect(arr.granted_token).to be_present + expect(result).to eq true + + result2 = AccountReset::GrantRequest.new(user2).call + arr2 = AccountResetRequest.find_by(user_id: user2.id) + expect(arr2.granted_at).to be_present + expect(arr2.granted_token).to be_present + expect(result2).to eq true + end + end + end + end + + def before_waiting_the_full_wait_period(now) + days = IdentityConfig.store.account_reset_wait_period_days.days + travel_to(now - 1 - days) do + yield + end + end + + def before_waiting_the_full_fraud_wait_period(now) + days = IdentityConfig.store.account_reset_fraud_user_wait_period_days.days + travel_to(now - 1 - days) do + yield + end end end diff --git a/spec/services/account_reset/grant_requests_and_send_emails_spec.rb b/spec/services/account_reset/grant_requests_and_send_emails_spec.rb index 54a8aab1093..5311dc5e3c3 100644 --- a/spec/services/account_reset/grant_requests_and_send_emails_spec.rb +++ b/spec/services/account_reset/grant_requests_and_send_emails_spec.rb @@ -10,62 +10,179 @@ let(:now) { Time.zone.now } context 'after waiting the full wait period' do - it 'does not send notifications when the notifications were already sent' do - before_waiting_the_full_wait_period(now) do - create_account_reset_request_for(user) + context 'standard user' do + it 'does not send notifications when the notifications were already sent' do + before_waiting_the_full_wait_period(now) do + create_account_reset_request_for(user) + end + + AccountReset::GrantRequestsAndSendEmails.new.perform(now) + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + expect(notifications_sent).to eq(0) end - AccountReset::GrantRequestsAndSendEmails.new.perform(now) - notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) - expect(notifications_sent).to eq(0) - end + it 'does not send notifications when the request was cancelled' do + before_waiting_the_full_wait_period(now) do + create_account_reset_request_for(user) + cancel_request_for(user) + end - it 'does not send notifications when the request was cancelled' do - before_waiting_the_full_wait_period(now) do - create_account_reset_request_for(user) - cancel_request_for(user) + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + expect(notifications_sent).to eq(0) end - notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) - expect(notifications_sent).to eq(0) - end + it 'sends notifications after a request is granted' do + before_waiting_the_full_wait_period(now) do + create_account_reset_request_for(user) + end - it 'sends notifications after a request is granted' do - before_waiting_the_full_wait_period(now) do - create_account_reset_request_for(user) + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + + expect(notifications_sent).to eq(1) end - notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + it 'sends 2 notifications after 2 requests are granted' do + before_waiting_the_full_wait_period(now) do + create_account_reset_request_for(user) + create_account_reset_request_for(user2) + end + + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) - expect(notifications_sent).to eq(1) + expect(notifications_sent).to eq(2) + end end - it 'sends 2 notifications after 2 requests are granted' do - before_waiting_the_full_wait_period(now) do - create_account_reset_request_for(user) - create_account_reset_request_for(user2) + context 'possible fraud user' do + let(:user) { create(:user, :fraud_review_pending) } + let(:user2) { create(:user, :fraud_rejection) } + before do + allow(IdentityConfig.store).to receive(:account_reset_fraud_user_wait_period_days). + and_return(10) + end + it 'does not send notifications when the notifications were already sent' do + before_waiting_the_full_fraud_wait_period(now) do + create_account_reset_request_for(user) + end + + AccountReset::GrantRequestsAndSendEmails.new.perform(now) + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + expect(notifications_sent).to eq(0) end - notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + it 'does not send notifications when the request was cancelled' do + before_waiting_the_full_fraud_wait_period(now) do + create_account_reset_request_for(user) + cancel_request_for(user) + end - expect(notifications_sent).to eq(2) + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + expect(notifications_sent).to eq(0) + end + + it 'sends notifications after a request is granted' do + before_waiting_the_full_fraud_wait_period(now) do + create_account_reset_request_for(user) + end + + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + + expect(notifications_sent).to eq(1) + end + + it 'sends 2 notifications after 2 requests are granted' do + before_waiting_the_full_fraud_wait_period(now) do + create_account_reset_request_for(user) + create_account_reset_request_for(user2) + end + + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + + expect(notifications_sent).to eq(2) + end end end context 'after not waiting the full wait period' do - it 'does not send notifications after a request' do - create_account_reset_request_for(user) + context 'standard user' do + it 'does not send notifications before a request wait period is done' do + create_account_reset_request_for(user) + + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + expect(notifications_sent).to eq(0) + end - notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) - expect(notifications_sent).to eq(0) + it 'does not send notifications when the request was cancelled' do + create_account_reset_request_for(user) + cancel_request_for(user) + + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + expect(notifications_sent).to eq(0) + end end - it 'does not send notifications when the request was cancelled' do - create_account_reset_request_for(user) - cancel_request_for(user) + context 'possible fraud user' do + let(:user) { create(:user, :fraud_review_pending) } + let(:user2) { create(:user, :fraud_rejection) } + context 'with fraud wait period set' do + it 'does not send notifications before a request wait period is done' do + create_account_reset_request_for(user) + + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + expect(notifications_sent).to eq(0) + end + + it 'does not send notifications when the request was cancelled' do + create_account_reset_request_for(user) + cancel_request_for(user) + + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + expect(notifications_sent).to eq(0) + end + + it 'should not send if its in between regular wait period and fraud wait period' do + before_waiting_the_full_wait_period(now) do + create_account_reset_request_for(user) + create_account_reset_request_for(user2) + end + + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + + expect(notifications_sent).to eq(0) + end + end + + context 'with fraud wait period not set' do + before do + allow(IdentityConfig.store).to receive(:account_reset_fraud_user_wait_period_days). + and_return(nil) + end + it 'does not send notifications before a request wait period is done' do + create_account_reset_request_for(user) - notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) - expect(notifications_sent).to eq(0) + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + expect(notifications_sent).to eq(0) + end + + it 'does not send notifications when the request was cancelled' do + create_account_reset_request_for(user) + cancel_request_for(user) + + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + expect(notifications_sent).to eq(0) + end + + it 'should send if its after regular wait period' do + before_waiting_the_full_wait_period(now) do + create_account_reset_request_for(user) + create_account_reset_request_for(user2) + end + + notifications_sent = AccountReset::GrantRequestsAndSendEmails.new.perform(now) + + expect(notifications_sent).to eq(2) + end + end end end end @@ -76,4 +193,11 @@ def before_waiting_the_full_wait_period(now) yield end end + + def before_waiting_the_full_fraud_wait_period(now) + days = IdentityConfig.store.account_reset_fraud_user_wait_period_days.days + travel_to(now - 1 - days) do + yield + end + end end