discourse/spec/requests/users_email_controller_spec.rb
Martin Brennan 9174716737
DEV: Remove Discourse.redis.delete_prefixed (#22103)
This method is a huge footgun in production, since it calls
the Redis KEYS command. From the Redis documentation at
https://redis.io/commands/keys/:

> Warning: consider KEYS as a command that should only be used in
production environments with extreme care. It may ruin performance when
it is executed against large databases. This command is intended for
debugging and special operations, such as changing your keyspace layout.
Don't use KEYS in your regular application code.

Since we were only using `delete_prefixed` in specs (now that we
removed the usage in production in 24ec06ff85)
we can remove this and instead rely on `use_redis_snapshotting` on the
particular tests that need this kind of clearing functionality.
2023-06-16 12:44:35 +10:00

457 lines
16 KiB
Ruby

# frozen_string_literal: true
require "rotp"
RSpec.describe UsersEmailController do
fab!(:user) { Fabricate(:user) }
let!(:email_token) { Fabricate(:email_token, user: user) }
fab!(:moderator) { Fabricate(:moderator) }
describe "#confirm-new-email" do
it "does not redirect to login for signed out accounts, this route works fine as anon user" do
get "/u/confirm-new-email/invalidtoken"
expect(response.status).to eq(200)
end
it "does not redirect to login for signed out accounts on login_required sites, this route works fine as anon user" do
SiteSetting.login_required = true
get "/u/confirm-new-email/invalidtoken"
expect(response.status).to eq(200)
end
it "errors out for invalid tokens" do
sign_in(user)
get "/u/confirm-new-email/invalidtoken"
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 for a signed in user" do
updater = EmailUpdater.new(guardian: user.guardian, user: user)
updater.change_to("bubblegum@adventuretime.ooo")
old_email = user.email
sign_in(moderator)
put "/u/confirm-new-email", params: { token: "#{email_token.token}" }
expect(user.reload.email).to eq(old_email)
end
context "with a valid user" do
let(:updater) { EmailUpdater.new(guardian: user.guardian, user: user) }
before do
sign_in(user)
updater.change_to("bubblegum@adventuretime.ooo")
end
it "includes security_key_allowed_credential_ids in a hidden field" do
key1 = Fabricate(:user_security_key_with_random_credential, user: user)
key2 = Fabricate(:user_security_key_with_random_credential, user: user)
get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
doc = Nokogiri.HTML5(response.body)
credential_ids = doc.css("#security-key-allowed-credential-ids").first["value"].split(",")
expect(credential_ids).to contain_exactly(key1.credential_id, key2.credential_id)
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: "#{updater.change_req.new_email_token.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("bubblegum@adventuretime.ooo")
end
context "when second factor is 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/#{updater.change_req.new_email_token.token}"
expect(response.status).to eq(200)
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/#{updater.change_req.new_email_token.token}?show_backup=true"
expect(response.status).to eq(200)
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: updater.change_req.new_email_token.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: updater.change_req.new_email_token.token,
}
expect(response.status).to eq(302)
expect(user.reload.email).to eq("bubblegum@adventuretime.ooo")
end
context "with rate limiting" do
before { RateLimiter.enable }
use_redis_snapshotting
it "rate limits by IP" do
freeze_time
6.times do
put "/u/confirm-new-email",
params: {
token: "blah",
second_factor_token: "000000",
second_factor_method: UserSecondFactor.methods[:totp],
}
expect(response.status).to eq(302)
end
put "/u/confirm-new-email",
params: {
token: "blah",
second_factor_token: "000000",
second_factor_method: UserSecondFactor.methods[:totp],
}
expect(response.status).to eq(429)
end
it "rate limits by username" do
freeze_time
6.times do |x|
user.email_change_requests.last.update(
change_state: EmailChangeRequest.states[:complete],
)
put "/u/confirm-new-email",
params: {
token: updater.change_req.new_email_token.token,
second_factor_token: "000000",
second_factor_method: UserSecondFactor.methods[:totp],
},
env: {
REMOTE_ADDR: "1.2.3.#{x}",
}
expect(response.status).to eq(302)
end
user.email_change_requests.last.update(
change_state: EmailChangeRequest.states[:authorizing_new],
)
put "/u/confirm-new-email",
params: {
token: updater.change_req.new_email_token.token,
second_factor_token: "000000",
second_factor_method: UserSecondFactor.methods[:totp],
},
env: {
REMOTE_ADDR: "1.2.3.4",
}
expect(response.status).to eq(429)
end
end
end
context "when security key is required" do
fab!(:user_security_key) do
Fabricate(
:user_security_key,
user: user,
credential_id: valid_security_key_data[:credential_id],
public_key: valid_security_key_data[:public_key],
)
end
before { simulate_localhost_webauthn_challenge }
it "requires a security key" do
get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
expect(response.status).to eq(200)
expect(response.body).to include(I18n.t("login.security_key_authenticate"))
expect(response.body).to include(I18n.t("login.security_key_description"))
end
context "if the user has a TOTP enabled and wants to use that instead" do
before { Fabricate(:user_second_factor_totp, user: user) }
it "allows entering the totp code instead" do
get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}?show_totp=true"
expect(response.status).to eq(200)
expect(response.body).to include(I18n.t("login.second_factor_title"))
expect(response.body).not_to include(I18n.t("login.security_key_authenticate"))
end
end
it "adds an error on a security key attempt" do
get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
put "/u/confirm-new-email",
params: {
token: updater.change_req.new_email_token.token,
second_factor_token: "{}",
second_factor_method: UserSecondFactor.methods[:security_key],
}
expect(response.status).to eq(302)
expect(flash[:invalid_second_factor]).to eq(true)
end
it "confirms with a correct security key token" do
get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
put "/u/confirm-new-email",
params: {
token: updater.change_req.new_email_token.token,
second_factor_token: valid_security_key_auth_post_data.to_json,
second_factor_method: UserSecondFactor.methods[:security_key],
}
expect(response.status).to eq(302)
expect(user.reload.email).to eq("bubblegum@adventuretime.ooo")
end
context "if the security key data JSON is garbled" do
it "raises an invalid parameters error" do
get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
put "/u/confirm-new-email",
params: {
token: updater.change_req.new_email_token.token,
second_factor_token: "{someweird: 8notjson}",
second_factor_method: UserSecondFactor.methods[:security_key],
}
expect(response.status).to eq(400)
end
end
end
end
it "destroys email tokens associated with the old email after the new email is confirmed" do
SiteSetting.enable_secondary_emails = true
email_token =
user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:password_reset])
updater = EmailUpdater.new(guardian: user.guardian, user: user)
updater.change_to("bubblegum@adventuretime.ooo")
sign_in(user)
put "/u/confirm-new-email", params: { token: "#{updater.change_req.new_email_token.token}" }
new_password = SecureRandom.hex
put "/u/password-reset/#{email_token.token}.json", params: { password: new_password }
expect(response.parsed_body["success"]).to eq(false)
expect(response.parsed_body["message"]).to eq(
I18n.t("password_reset.no_token", base_url: Discourse.base_url),
)
expect(user.reload.confirm_password?(new_password)).to eq(false)
end
end
describe "#confirm-old-email" do
it "redirects to login for signed out accounts" do
get "/u/confirm-old-email/invalidtoken"
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/invalidtoken"
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(guardian: moderator.guardian, user: moderator)
email_change_request = updater.change_to("bubblegum@adventuretime.ooo")
get "/u/confirm-old-email/#{email_change_request.old_email_token.token}"
expect(response.status).to eq(200)
expect(body).to include("alert-error")
end
context "with valid old token" do
it "confirms with a correct token" do
sign_in(moderator)
updater = EmailUpdater.new(guardian: moderator.guardian, user: moderator)
email_change_request = updater.change_to("bubblegum@adventuretime.ooo")
get "/u/confirm-old-email/#{email_change_request.old_email_token.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: email_change_request.old_email_token.token }
expect(response.status).to eq(302)
expect(response.redirect_url).to include("done=true")
end
end
end
describe "#create" do
it "has an email token" do
sign_in(user)
expect {
post "/u/#{user.username}/preferences/email.json",
params: {
email: "bubblegum@adventuretime.ooo",
}
}.to change(EmailChangeRequest, :count)
emailChangeRequest = EmailChangeRequest.last
expect(emailChangeRequest.old_email).to eq(nil)
expect(emailChangeRequest.new_email).to eq("bubblegum@adventuretime.ooo")
end
end
describe "#update" do
it "requires you to be logged in" do
put "/u/#{user.username}/preferences/email.json",
params: {
email: "bubblegum@adventuretime.ooo",
}
expect(response.status).to eq(403)
end
context "when logged in" do
before { sign_in(user) }
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
SiteSetting.email_editable = false
put "/u/#{user.username}/preferences/email.json",
params: {
email: "bubblegum@adventuretime.ooo",
}
expect(response).to be_forbidden
end
context "when the new email address is taken" do
fab!(:other_user) { Fabricate(:coding_horror) }
context "when hide_email_address_taken is disabled" do
before { SiteSetting.hide_email_address_taken = false }
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 "when hide_email_address_taken is enabled" do
before { SiteSetting.hide_email_address_taken = true }
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 blocked_email_domains site setting" do
SiteSetting.blocked_email_domains = "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 allowed_email_domains site setting" do
SiteSetting.allowed_email_domains = "discourse.org"
put "/u/#{user.username}/preferences/email.json",
params: {
email: "bubblegum@adventuretime.ooo",
}
expect(response).to_not be_successful
end
context "with success" do
it "has an email token" do
expect do
put "/u/#{user.username}/preferences/email.json",
params: {
email: "bubblegum@adventuretime.ooo",
}
end.to change(EmailChangeRequest, :count)
end
end
end
end
end