discourse/spec/requests/users_email_controller_spec.rb
Sam Saffron b57e108e84 FEATURE: improve email change workflow
- Show old and new email address during the process
- Ensure correct user is logged on when attempting to make email changes
- Support reloading a page during the email reset process without resubmit
of form
- Improve tests
- Fixed issue where redirect back to site was not linking correctly in
subfolder setups

Internal refactor of single action into 4 distinct actions that are simpler
to reason about.

This also removes the step that logs on an account after you confirm an
email change, since it is no longer needed which leaves us with safer
internals.

This left me no choice but to amend translations cause the old route was
removed.
2019-11-21 16:28:35 +11:00

299 lines
8.7 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
describe UsersEmailController do
fab!(:user) { Fabricate(:user) }
fab!(:moderator) { Fabricate(:moderator) }
describe "#confirm-new-email" do
it 'redirects to login for signed out accounts' do
get "/u/confirm-new-email/asdfasdf"
expect(response.status).to eq(302)
expect(response.redirect_url).to eq("http://test.localhost/login")
end
it 'errors out for invalid tokens' do
sign_in(user)
get "/u/confirm-new-email/asdfasdf"
expect(response.status).to eq(200)
expect(response.body).to include(I18n.t('change_email.already_done'))
end
it 'does not change email if accounts mismatch' do
updater = EmailUpdater.new(user.guardian, user)
updater.change_to('new.n.cool@example.com')
old_email = user.email
sign_in(moderator)
put "/u/confirm-new-email", params: {
token: "#{user.email_tokens.last.token}"
}
user.reload
expect(user.email).to eq(old_email)
end
context "with a valid user" do
let(:updater) { EmailUpdater.new(user.guardian, user) }
before do
sign_in(user)
updater.change_to('new.n.cool@example.com')
end
it 'confirms with a correct token' do
user.user_stat.update_columns(bounce_score: 42, reset_bounce_score_after: 1.week.from_now)
put "/u/confirm-new-email", params: {
token: "#{user.email_tokens.last.token}"
}
expect(response.status).to eq(302)
expect(response.redirect_url).to include("done")
user.reload
expect(user.user_stat.bounce_score).to eq(0)
expect(user.user_stat.reset_bounce_score_after).to eq(nil)
expect(user.email).to eq("new.n.cool@example.com")
end
context 'second factor required' do
fab!(:second_factor) { Fabricate(:user_second_factor_totp, user: user) }
fab!(:backup_code) { Fabricate(:user_second_factor_backup, user: user) }
it 'requires a second factor token' do
get "/u/confirm-new-email/#{user.email_tokens.last.token}"
expect(response.status).to eq(200)
response_body = response.body
expect(response_body).to include(I18n.t("login.second_factor_title"))
expect(response_body).not_to include(I18n.t("login.invalid_second_factor_code"))
end
it 'requires a backup token' do
get "/u/confirm-new-email/#{user.email_tokens.last.token}?show_backup=true"
expect(response.status).to eq(200)
response_body = response.body
expect(response_body).to include(I18n.t("login.second_factor_backup_title"))
end
it 'adds an error on a second factor attempt' do
put "/u/confirm-new-email", params: {
token: user.email_tokens.last.token,
second_factor_token: "000000",
second_factor_method: UserSecondFactor.methods[:totp]
}
expect(response.status).to eq(302)
expect(flash[:invalid_second_factor]).to eq(true)
end
it 'confirms with a correct second token' do
put "/u/confirm-new-email", params: {
second_factor_token: ROTP::TOTP.new(second_factor.data).now,
second_factor_method: UserSecondFactor.methods[:totp],
token: user.email_tokens.last.token
}
expect(response.status).to eq(302)
user.reload
expect(user.email).to eq("new.n.cool@example.com")
end
end
end
end
describe '#confirm-old-email' do
it 'redirects to login for signed out accounts' do
get "/u/confirm-old-email/asdfasdf"
expect(response.status).to eq(302)
expect(response.redirect_url).to eq("http://test.localhost/login")
end
it 'errors out for invalid tokens' do
sign_in(user)
get "/u/confirm-old-email/asdfasdf"
expect(response.status).to eq(200)
expect(response.body).to include(I18n.t('change_email.already_done'))
end
it 'bans change when accounts do not match' do
sign_in(user)
updater = EmailUpdater.new(moderator.guardian, moderator)
updater.change_to('new.n.cool@example.com')
get "/u/confirm-old-email/#{moderator.email_tokens.last.token}"
expect(response.status).to eq(200)
expect(body).to include("alert-error")
end
context 'valid old address token' do
it 'confirms with a correct token' do
# NOTE: only moderators need to confirm both old and new
sign_in(moderator)
updater = EmailUpdater.new(moderator.guardian, moderator)
updater.change_to('new.n.cool@example.com')
get "/u/confirm-old-email/#{moderator.email_tokens.last.token}"
expect(response.status).to eq(200)
body = CGI.unescapeHTML(response.body)
expect(body)
.to include(I18n.t('change_email.authorizing_old.title'))
expect(body)
.to include(I18n.t('change_email.authorizing_old.description'))
put "/u/confirm-old-email", params: {
token: moderator.email_tokens.last.token
}
expect(response.status).to eq(302)
expect(response.redirect_url).to include("done=true")
end
end
end
describe '#update' do
let(:new_email) { 'bubblegum@adventuretime.ooo' }
it "requires you to be logged in" do
put "/u/#{user.username}/preferences/email.json", params: { email: new_email }
expect(response.status).to eq(403)
end
context 'when logged in' do
before do
sign_in(user)
end
it 'raises an error without an email parameter' do
put "/u/#{user.username}/preferences/email.json"
expect(response.status).to eq(400)
end
it 'raises an error without an invalid email' do
put "/u/#{user.username}/preferences/email.json", params: { email: "sam@not-email.com'" }
expect(response.status).to eq(422)
expect(response.body).to include("email is invalid")
end
it "raises an error if you can't edit the user's email" do
Guardian.any_instance.expects(:can_edit_email?).with(user).returns(false)
put "/u/#{user.username}/preferences/email.json", params: { email: new_email }
expect(response).to be_forbidden
end
context 'when the new email address is taken' do
fab!(:other_user) { Fabricate(:coding_horror) }
context 'hide_email_address_taken is disabled' do
before do
SiteSetting.hide_email_address_taken = false
end
it 'raises an error' do
put "/u/#{user.username}/preferences/email.json", params: {
email: other_user.email
}
expect(response).to_not be_successful
end
it 'raises an error if there is whitespace too' do
put "/u/#{user.username}/preferences/email.json", params: {
email: "#{other_user.email} "
}
expect(response).to_not be_successful
end
end
context 'hide_email_address_taken is enabled' do
before do
SiteSetting.hide_email_address_taken = true
end
it 'responds with success' do
put "/u/#{user.username}/preferences/email.json", params: {
email: other_user.email
}
expect(response.status).to eq(200)
end
end
end
context 'when new email is different case of existing email' do
fab!(:other_user) { Fabricate(:user, email: 'case.insensitive@gmail.com') }
it 'raises an error' do
put "/u/#{user.username}/preferences/email.json", params: {
email: other_user.email.upcase
}
expect(response).to_not be_successful
end
end
it 'raises an error when new email domain is present in email_domains_blacklist site setting' do
SiteSetting.email_domains_blacklist = "mailinator.com"
put "/u/#{user.username}/preferences/email.json", params: {
email: "not_good@mailinator.com"
}
expect(response).to_not be_successful
end
it 'raises an error when new email domain is not present in email_domains_whitelist site setting' do
SiteSetting.email_domains_whitelist = "discourse.org"
put "/u/#{user.username}/preferences/email.json", params: {
email: new_email
}
expect(response).to_not be_successful
end
context 'success' do
it 'has an email token' do
expect do
put "/u/#{user.username}/preferences/email.json", params: {
email: new_email
}
end.to change(EmailChangeRequest, :count)
end
end
end
end
end