Skip to content
Merged
7 changes: 6 additions & 1 deletion app/controllers/api/verify/password_reset_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ def create

def reset_password(email, request_id)
sign_out
RequestPasswordReset.new(email: email, request_id: request_id, analytics: analytics).perform
RequestPasswordReset.new(
email: email,
request_id: request_id,
analytics: analytics,
irs_attempts_api_tracker: irs_attempts_api_tracker,
).perform
session[:email] = email
end
end
Expand Down
7 changes: 6 additions & 1 deletion app/controllers/idv/forgot_password_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ def update

def reset_password(email, request_id)
sign_out
RequestPasswordReset.new(email: email, request_id: request_id, analytics: analytics).perform
RequestPasswordReset.new(
email: email,
request_id: request_id,
analytics: analytics,
irs_attempts_api_tracker: irs_attempts_api_tracker,
).perform
# The user/email is always found so...
session[:email] = email
redirect_to forgot_password_url(request_id: request_id)
Expand Down
1 change: 1 addition & 0 deletions app/controllers/users/reset_passwords_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def create_account_if_email_not_found
email: email,
request_id: request_id,
analytics: analytics,
irs_attempts_api_tracker: irs_attempts_api_tracker,
).perform

return unless result
Expand Down
20 changes: 20 additions & 0 deletions app/services/irs_attempts_api/tracker_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,26 @@ def email_and_password_auth(email:, success:)
)
end

# The user has exceeded the rate limit for password reset emails
# @param [String] email The user's email address
def forgot_password_email_rate_limited(email:)
track_event(
:forgot_password_email_rate_limited,
email: email,
)
end

# Tracks when the user has requested a forgot password email
# @param [String] email The submitted email address
# @param [Boolean] success True if the forgot password email was sent
def forgot_password_email_sent(email:, success:)
track_event(
:forgot_password_email_sent,
email: email,
success: success,
)
end

# @param [Boolean] success
# @param [Hash<Symbol,Array<Symbol>>] failure_reason
def forgot_password_email_confirmed(success:, failure_reason: nil)
Expand Down
8 changes: 6 additions & 2 deletions app/services/request_password_reset.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
RequestPasswordReset = RedactedStruct.new(
:email, :request_id, :analytics, keyword_init: true,
allowed_members: [:request_id]
:email, :request_id, :analytics, :irs_attempts_api_tracker,
keyword_init: true,
allowed_members: [:request_id]
) do
def perform
if user_should_receive_registration_email?
Expand All @@ -18,12 +19,15 @@ def perform
def send_reset_password_instructions
if Throttle.new(user: user, throttle_type: :reset_password_email).throttled_else_increment?
analytics.throttler_rate_limit_triggered(throttle_type: :reset_password_email)
irs_attempts_api_tracker.forgot_password_email_rate_limited(email: email)
else
token = user.set_reset_password_token
UserMailer.reset_password_instructions(user, email, token: token).deliver_now_or_later

event = PushNotification::RecoveryActivatedEvent.new(user: user)
PushNotification::HttpPush.deliver(event)

irs_attempts_api_tracker.forgot_password_email_sent(email: email, success: true)
end
end

Expand Down
27 changes: 21 additions & 6 deletions spec/controllers/idv/forgot_password_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,40 @@
end

describe '#new' do
it 'tracks the event in analytics when referer is nil' do
before do
stub_sign_in
stub_analytics

expect(@analytics).to receive(:track_event).with('IdV: forgot password visited')
allow(@analytics).to receive(:track_event)
end

it 'tracks the event in analytics when referer is nil' do
get :new

expect(@analytics).to have_received(:track_event).with('IdV: forgot password visited')
end
end

describe '#update' do
it 'tracks an analytics event' do
user = create(:user)
let(:user) { create(:user) }

before do
stub_sign_in(user)
stub_analytics
stub_attempts_tracker
allow(@analytics).to receive(:track_event)
allow(@irs_attempts_api_tracker).to receive(:track_event)
end

expect(@analytics).to receive(:track_event).with('IdV: forgot password confirmed')

it 'tracks analytics events' do
post :update

expect(@analytics).to have_received(:track_event).with('IdV: forgot password confirmed')
expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
:forgot_password_email_sent,
email: user.email,
success: true,
)
end
end
end
137 changes: 90 additions & 47 deletions spec/controllers/users/reset_passwords_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,25 @@
"This password is too short (minimum is #{Devise.password_length.first} characters)"
end
describe '#edit' do
context 'no user matches token' do
it 'redirects to page where user enters email for password reset token' do
stub_analytics
stub_attempts_tracker
allow(@analytics).to receive(:track_event)
allow(@irs_attempts_api_tracker).to receive(:track_event)

get :edit, params: { reset_password_token: 'foo' }
before do
stub_analytics
stub_attempts_tracker
allow(@analytics).to receive(:track_event)
allow(@irs_attempts_api_tracker).to receive(:track_event)
end

analytics_hash = {
context 'no user matches token' do
let(:analytics_hash) do
{
success: false,
errors: { user: ['invalid_token'] },
error_details: { user: [:blank] },
user_id: nil,
}
end

it 'redirects to page where user enters email for password reset token' do
get :edit, params: { reset_password_token: 'foo' }

expect(@analytics).to have_received(:track_event).
with('Password Reset: Token Submitted', analytics_hash)
Expand All @@ -35,24 +39,23 @@
end

context 'token expired' do
it 'redirects to page where user enters email for password reset token' do
stub_analytics
stub_attempts_tracker
allow(@analytics).to receive(:track_event)
allow(@irs_attempts_api_tracker).to receive(:track_event)

user = instance_double('User', uuid: '123')
allow(User).to receive(:with_reset_password_token).with('foo').and_return(user)
allow(user).to receive(:reset_password_period_valid?).and_return(false)

get :edit, params: { reset_password_token: 'foo' }

analytics_hash = {
let(:analytics_hash) do
{
success: false,
errors: { user: ['token_expired'] },
error_details: { user: ['token_expired'] },
user_id: '123',
}
end
let(:user) { instance_double('User', uuid: '123') }

before do
allow(User).to receive(:with_reset_password_token).with('foo').and_return(user)
allow(user).to receive(:reset_password_period_valid?).and_return(false)
end

it 'redirects to page where user enters email for password reset token' do
get :edit, params: { reset_password_token: 'foo' }

expect(@analytics).to have_received(:track_event).
with('Password Reset: Token Submitted', analytics_hash)
Expand All @@ -68,17 +71,17 @@

context 'token is valid' do
render_views
let(:user) { instance_double('User', uuid: '123') }
let(:email_address) { instance_double('EmailAddress') }

it 'displays the form to enter a new password and disallows indexing' do
before do
stub_analytics
stub_attempts_tracker
allow(@irs_attempts_api_tracker).to receive(:track_event)

user = instance_double('User', uuid: '123')
email_address = instance_double('EmailAddress')
allow(User).to receive(:with_reset_password_token).with('foo').and_return(user)
allow(user).to receive(:reset_password_period_valid?).and_return(true)
allow(user).to receive(:email_addresses).and_return([email_address])
end

it 'displays the form to enter a new password and disallows indexing' do
expect(email_address).to receive(:email).twice

forbidden = instance_double(ForbiddenPasswords)
Expand Down Expand Up @@ -376,11 +379,12 @@

describe '#create' do
context 'no user matches email' do
let(:email) { 'nonexistent@example.com' }

it 'send an email to tell the user they do not have an account yet' do
stub_analytics
stub_attempts_tracker
allow(@irs_attempts_api_tracker).to receive(:track_event)
email = 'nonexistent@example.com'

expect do
put :create, params: {
Expand Down Expand Up @@ -422,61 +426,94 @@
end

context 'user exists' do
it 'sends password reset email to user and tracks event' do
stub_analytics

user = create(:user, :signed_up, email: 'test@example.com')

analytics_hash = {
let(:email) { 'test@example.com' }
let!(:user) { create(:user, :signed_up, email: email) }
let(:analytics_hash) do
{
success: true,
errors: {},
user_id: user.uuid,
confirmed: true,
active_profile: false,
}
end

expect(@analytics).to receive(:track_event).
with('Password Reset: Email Submitted', analytics_hash)
before do
stub_analytics
stub_attempts_tracker
allow(@analytics).to receive(:track_event)
allow(@irs_attempts_api_tracker).to receive(:track_event)
end

it 'sends password reset email to user and tracks event' do
expect do
put :create, params: { password_reset_email_form: { email: 'Test@example.com' } }
put :create, params: { password_reset_email_form: { email: email } }
end.to change { ActionMailer::Base.deliveries.count }.by(1)

expect(@analytics).to have_received(:track_event).
with('Password Reset: Email Submitted', analytics_hash)

expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
:forgot_password_email_sent,
email: email,
success: true,
)

expect(response).to redirect_to forgot_password_path
end
end

context 'user exists but is unconfirmed' do
it 'sends password reset email to user and tracks event' do
stub_analytics

user = create(:user, :unconfirmed)

analytics_hash = {
let(:user) { create(:user, :unconfirmed) }
let(:analytics_hash) do
{
success: true,
errors: {},
user_id: user.uuid,
confirmed: false,
active_profile: false,
}
end
let(:params) do
{
password_reset_email_form: {
email: user.email,
},
}
end

expect(@analytics).to receive(:track_event).
with('Password Reset: Email Submitted', analytics_hash)
before do
stub_analytics
stub_attempts_tracker
allow(@analytics).to receive(:track_event)
allow(@irs_attempts_api_tracker).to receive(:track_event)
end

params = { password_reset_email_form: { email: user.email } }
it 'sends password reset email to user and tracks event' do
expect { put :create, params: params }.
to change { ActionMailer::Base.deliveries.count }.by(1)

expect(@analytics).to have_received(:track_event).
with('Password Reset: Email Submitted', analytics_hash)

expect(ActionMailer::Base.deliveries.last.subject).
to eq t('user_mailer.reset_password_instructions.subject')

expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
:forgot_password_email_sent,
email: user.email,
success: true,
)

expect(response).to redirect_to forgot_password_path
end
end

context 'user is verified' do
it 'captures in analytics that the user was verified' do
stub_analytics
stub_attempts_tracker
allow(@irs_attempts_api_tracker).to receive(:track_event)

user = create(:user, :signed_up)
create(:profile, :active, :verified, user: user)
Expand All @@ -494,6 +531,12 @@

params = { password_reset_email_form: { email: user.email } }
put :create, params: params

expect(@irs_attempts_api_tracker).to have_received(:track_event).with(
:forgot_password_email_sent,
email: user.email,
success: true,
)
end
end

Expand Down
Loading