discourse/spec/requests/users_controller_spec.rb
Martin Brennan 29ccdf5d35
FIX: Show a nicer error if name/code missing for TOTP/Security Keys (#9124)
Meta: https://meta.discourse.org/t/improve-error-message-when-not-including-name-setting-up-totp/143339

* when the user creates a TOTP second factor method we want
to show them a nicer error if they forget to add a name
or the code from the app, instead of the param missing error
* also add a client-side check for this and for security key name,
no need to bother the server if we can help it
2020-03-06 14:37:40 +10:00

4125 lines
135 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
require 'rotp'
describe UsersController do
let(:user) { Fabricate(:user) }
describe "#full account registration flow" do
it "will correctly handle honeypot and challenge" do
get '/u/hp.json'
expect(response.status).to eq(200)
json = JSON.parse(response.body)
params = {
email: 'jane@jane.com',
name: 'jane',
username: 'jane',
password_confirmation: json['value'],
challenge: json['challenge'].reverse,
password: SecureRandom.hex
}
secure_session = SecureSession.new(session["secure_session_id"])
expect(secure_session[UsersController::HONEYPOT_KEY]).to eq(json["value"])
expect(secure_session[UsersController::CHALLENGE_KEY]).to eq(json["challenge"])
post '/u.json', params: params
expect(response.status).to eq(200)
jane = User.find_by(username: 'jane')
expect(jane.email).to eq('jane@jane.com')
expect(secure_session[UsersController::HONEYPOT_KEY]).to eq(nil)
expect(secure_session[UsersController::CHALLENGE_KEY]).to eq(nil)
end
end
describe '#perform_account_activation' do
let(:token) do
return @token if @token.present?
email_token = EmailToken.create!(expired: false, confirmed: false, user: user, email: user.email)
@token = email_token.token
@token
end
before do
UsersController.any_instance.stubs(:honeypot_or_challenge_fails?).returns(false)
end
context 'invalid token' do
it 'return success' do
put "/u/activate-account/invalid-tooken"
expect(response.status).to eq(200)
expect(flash[:error]).to be_present
end
end
context 'valid token' do
context 'welcome message' do
it 'enqueues a welcome message if the user object indicates so' do
user.update(active: false)
put "/u/activate-account/#{token}"
expect(response.status).to eq(200)
expect(Jobs::SendSystemMessage.jobs.size).to eq(1)
expect(Jobs::SendSystemMessage.jobs.first["args"].first["message_type"]).to eq("welcome_user")
end
it "doesn't enqueue the welcome message if the object returns false" do
user.update(active: true)
put "/u/activate-account/#{token}"
expect(response.status).to eq(200)
expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
end
end
context "honeypot" do
it "raises an error if the honeypot is invalid" do
UsersController.any_instance.stubs(:honeypot_or_challenge_fails?).returns(true)
put "/u/activate-account/#{token}"
expect(response.status).to eq(403)
end
end
context 'response' do
before do
Guardian.any_instance.expects(:can_access_forum?).returns(true)
EmailToken.expects(:confirm).with("#{token}").returns(user)
end
it 'correctly logs on user' do
events = DiscourseEvent.track_events do
put "/u/activate-account/#{token}"
end
expect(events.map { |event| event[:event_name] }).to contain_exactly(
:user_logged_in, :user_first_logged_in
)
expect(response.status).to eq(200)
expect(flash[:error]).to be_blank
expect(session[:current_user_id]).to be_present
expect(CGI.unescapeHTML(response.body))
.to_not include(I18n.t('activation.approval_required'))
end
end
context 'user is not approved' do
before do
SiteSetting.must_approve_users = true
EmailToken.expects(:confirm).with("#{token}").returns(user)
put "/u/activate-account/#{token}"
end
it 'should return the right response' do
expect(response.status).to eq(200)
expect(CGI.unescapeHTML(response.body))
.to include(I18n.t('activation.approval_required'))
expect(response.body).to_not have_tag(:script, with: {
src: '/assets/application.js'
})
expect(flash[:error]).to be_blank
expect(session[:current_user_id]).to be_blank
end
end
end
context 'when cookies contains a destination URL' do
it 'should redirect to the URL' do
destination_url = 'http://thisisasite.com/somepath'
cookies[:destination_url] = destination_url
put "/u/activate-account/#{token}"
expect(response).to redirect_to(destination_url)
end
end
end
describe '#password_reset' do
fab!(:user) { Fabricate(:user) }
let(:token) { SecureRandom.hex }
context "you can view it even if login is required" do
it "returns success" do
SiteSetting.login_required = true
get "/u/password-reset/#{token}"
expect(response.status).to eq(200)
expect(CGI.unescapeHTML(response.body)).to include(I18n.t('password_reset.no_token'))
end
end
context 'missing token' do
it 'disallows login' do
get "/u/password-reset/#{token}"
expect(response.status).to eq(200)
expect(CGI.unescapeHTML(response.body))
.to include(I18n.t('password_reset.no_token'))
expect(response.body).to_not have_tag(:script, with: {
src: '/assets/application.js'
})
expect(session[:current_user_id]).to be_blank
end
it "responds with proper error message" do
get "/u/password-reset/#{token}.json"
expect(response.status).to eq(200)
expect(JSON.parse(response.body)["message"]).to eq(I18n.t('password_reset.no_token'))
expect(session[:current_user_id]).to be_blank
end
end
context 'invalid token' do
it 'disallows login' do
get "/u/password-reset/ev!l_trout@!"
expect(response.status).to eq(200)
expect(CGI.unescapeHTML(response.body))
.to include(I18n.t('password_reset.no_token'))
expect(response.body).to_not have_tag(:script, with: {
src: '/assets/application.js'
})
expect(session[:current_user_id]).to be_blank
end
it "responds with proper error message" do
put "/u/password-reset/evil_trout!.json", params: { password: "awesomeSecretPassword" }
expect(response.status).to eq(200)
expect(JSON.parse(response.body)["message"]).to eq(I18n.t('password_reset.no_token'))
expect(session[:current_user_id]).to be_blank
end
end
context 'valid token' do
context 'when rendered' do
it 'renders referrer never on get requests' do
user = Fabricate(:user)
token = user.email_tokens.create(email: user.email).token
get "/u/password-reset/#{token}"
expect(response.status).to eq(200)
expect(response.body).to include('<meta name="referrer" content="never">')
end
end
it 'returns success' do
user = Fabricate(:user)
user_auth_token = UserAuthToken.generate!(user_id: user.id)
token = user.email_tokens.create(email: user.email).token
events = DiscourseEvent.track_events do
put "/u/password-reset/#{token}", params: { password: 'hg9ow8yhg98o' }
end
expect(events.map { |event| event[:event_name] }).to contain_exactly(
:user_logged_in, :user_first_logged_in
)
expect(response.status).to eq(200)
expect(response.body).to have_tag("div#data-preloaded") do |element|
json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
expect(json['password_reset']).to include('{"is_developer":false,"admin":false,"second_factor_required":false,"security_key_required":false,"backup_enabled":false,"multiple_second_factor_methods":false}')
end
expect(session["password-#{token}"]).to be_blank
expect(UserAuthToken.where(id: user_auth_token.id).count).to eq(0)
end
it 'disallows double password reset' do
user = Fabricate(:user)
token = user.email_tokens.create(email: user.email).token
put "/u/password-reset/#{token}", params: { password: 'hg9ow8yHG32O' }
put "/u/password-reset/#{token}", params: { password: 'test123987AsdfXYZ' }
user.reload
expect(user.confirm_password?('hg9ow8yHG32O')).to eq(true)
# logged in now
expect(user.user_auth_tokens.count).to eq(1)
end
it "doesn't redirect to wizard on get" do
user = Fabricate(:admin)
UserAuthToken.generate!(user_id: user.id)
token = user.email_tokens.create(email: user.email).token
get "/u/password-reset/#{token}.json"
expect(response).not_to redirect_to(wizard_path)
end
it "redirects to the wizard if you're the first admin" do
user = Fabricate(:admin)
UserAuthToken.generate!(user_id: user.id)
token = user.email_tokens.create(email: user.email).token
get "/u/password-reset/#{token}"
put "/u/password-reset/#{token}", params: { password: 'hg9ow8yhg98oadminlonger' }
expect(response).to redirect_to(wizard_path)
end
it "logs the password change" do
user = Fabricate(:admin)
UserAuthToken.generate!(user_id: user.id)
token = user.email_tokens.create(email: user.email).token
get "/u/password-reset/#{token}"
expect do
put "/u/password-reset/#{token}", params: { password: 'hg9ow8yhg98oadminlonger' }
end.to change { UserHistory.count }.by (1)
entry = UserHistory.last
expect(entry.target_user_id).to eq(user.id)
expect(entry.action).to eq(UserHistory.actions[:change_password])
end
it "doesn't invalidate the token when loading the page" do
user = Fabricate(:user)
user_token = UserAuthToken.generate!(user_id: user.id)
email_token = user.email_tokens.create(email: user.email)
get "/u/password-reset/#{email_token.token}.json"
expect(response.status).to eq(200)
email_token.reload
expect(email_token.confirmed).to eq(false)
expect(UserAuthToken.where(id: user_token.id).count).to eq(1)
end
context "rate limiting" do
before { RateLimiter.clear_all!; RateLimiter.enable }
after { RateLimiter.disable }
it "rate limits reset passwords" do
freeze_time
token = user.email_tokens.create!(email: user.email).token
3.times do
put "/u/password-reset/#{token}", params: {
second_factor_token: 123456,
second_factor_method: 1
}
expect(response.status).to eq(200)
end
put "/u/password-reset/#{token}", params: {
second_factor_token: 123456,
second_factor_method: 1
}
expect(response.status).to eq(429)
end
end
context '2 factor authentication required' do
let!(:second_factor) { Fabricate(:user_second_factor_totp, user: user) }
it 'does not change with an invalid token' do
token = user.email_tokens.create!(email: user.email).token
get "/u/password-reset/#{token}"
expect(response.body).to have_tag("div#data-preloaded") do |element|
json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
expect(json['password_reset']).to include('{"is_developer":false,"admin":false,"second_factor_required":true,"security_key_required":false,"backup_enabled":false,"multiple_second_factor_methods":false}')
end
put "/u/password-reset/#{token}", params: {
password: 'hg9ow8yHG32O',
second_factor_token: '000000',
second_factor_method: UserSecondFactor.methods[:totp]
}
expect(response.body).to include(I18n.t("login.invalid_second_factor_code"))
user.reload
expect(user.confirm_password?('hg9ow8yHG32O')).not_to eq(true)
expect(user.user_auth_tokens.count).not_to eq(1)
end
it 'changes password with valid 2-factor tokens' do
token = user.email_tokens.create(email: user.email).token
get "/u/password-reset/#{token}"
put "/u/password-reset/#{token}", params: {
password: 'hg9ow8yHG32O',
second_factor_token: ROTP::TOTP.new(second_factor.data).now,
second_factor_method: UserSecondFactor.methods[:totp]
}
user.reload
expect(response.status).to eq(200)
expect(user.confirm_password?('hg9ow8yHG32O')).to eq(true)
expect(user.user_auth_tokens.count).to eq(1)
end
end
context 'security key authentication required' do
let!(: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
let(:token) { user.email_tokens.create!(email: user.email).token }
before do
simulate_localhost_webauthn_challenge
# store challenge in secure session by visiting the email login page
get "/u/password-reset/#{token}"
end
it 'preloads with a security key challenge and allowed credential ids' do
expect(response.body).to have_tag("div#data-preloaded") do |element|
json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
password_reset = JSON.parse(json['password_reset'])
expect(password_reset['challenge']).not_to eq(nil)
expect(password_reset['allowed_credential_ids']).to eq([user_security_key.credential_id])
expect(password_reset['security_key_required']).to eq(true)
end
end
it 'stages a webauthn challenge and rp-id for the user' do
secure_session = SecureSession.new(session["secure_session_id"])
expect(Webauthn.challenge(user, secure_session)).not_to eq(nil)
expect(Webauthn.rp_id(user, secure_session)).to eq(Discourse.current_hostname)
end
it 'changes password with valid security key challenge and authentication' do
put "/u/password-reset/#{token}.json", params: {
password: 'hg9ow8yHG32O',
second_factor_token: valid_security_key_auth_post_data,
second_factor_method: UserSecondFactor.methods[:security_key]
}
user.reload
expect(response.status).to eq(200)
expect(user.confirm_password?('hg9ow8yHG32O')).to eq(true)
expect(user.user_auth_tokens.count).to eq(1)
end
it "does not change a password if a fake TOTP token is provided" do
put "/u/password-reset/#{token}.json", params: {
password: 'hg9ow8yHG32O',
second_factor_token: 'blah',
second_factor_method: UserSecondFactor.methods[:security_key]
}
user.reload
expect(response.status).to eq(200)
expect(user.confirm_password?('hg9ow8yHG32O')).to eq(false)
end
context "when security key authentication fails" do
it 'shows an error message and does not change password' do
put "/u/password-reset/#{token}", params: {
password: 'hg9ow8yHG32O',
second_factor_token: {
signature: 'bad',
clientData: 'bad',
authenticatorData: 'bad',
credentialId: 'bad'
},
second_factor_method: UserSecondFactor.methods[:security_key]
}
user.reload
expect(user.confirm_password?('hg9ow8yHG32O')).to eq(false)
expect(response.status).to eq(200)
expect(response.body).to include(I18n.t("webauthn.validation.not_found_error"))
end
end
end
end
context 'submit change' do
let(:token) { EmailToken.generate_token }
before do
EmailToken.expects(:confirm).with(token).returns(user)
end
it "fails when the password is blank" do
put "/u/password-reset/#{token}.json", params: { password: '' }
expect(response.status).to eq(200)
expect(JSON.parse(response.body)["errors"]).to be_present
expect(session[:current_user_id]).to be_blank
end
it "fails when the password is too long" do
put "/u/password-reset/#{token}.json", params: { password: ('x' * (User.max_password_length + 1)) }
expect(response.status).to eq(200)
expect(JSON.parse(response.body)["errors"]).to be_present
expect(session[:current_user_id]).to be_blank
end
it "logs in the user" do
put "/u/password-reset/#{token}.json", params: { password: 'ksjafh928r' }
expect(response.status).to eq(200)
expect(JSON.parse(response.body)["errors"]).to be_blank
expect(session[:current_user_id]).to be_present
end
it "doesn't log in the user when not approved" do
SiteSetting.must_approve_users = true
put "/u/password-reset/#{token}.json", params: { password: 'ksjafh928r' }
expect(JSON.parse(response.body)["errors"]).to be_blank
expect(session[:current_user_id]).to be_blank
end
end
end
describe '#confirm_email_token' do
fab!(:user) { Fabricate(:user) }
it "token doesn't match any records" do
email_token = user.email_tokens.create(email: user.email)
get "/u/confirm-email-token/#{SecureRandom.hex}.json"
expect(response.status).to eq(200)
expect(email_token.reload.confirmed).to eq(false)
end
it "token matches" do
email_token = user.email_tokens.create(email: user.email)
get "/u/confirm-email-token/#{email_token.token}.json"
expect(response.status).to eq(200)
expect(email_token.reload.confirmed).to eq(true)
end
end
describe '#admin_login' do
fab!(:admin) { Fabricate(:admin) }
fab!(:user) { Fabricate(:user) }
context 'enqueues mail' do
it 'enqueues mail with admin email and sso enabled' do
put "/u/admin-login", params: { email: admin.email }
expect(response.status).to eq(200)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(1)
args = Jobs::CriticalUserEmail.jobs.first["args"].first
expect(args["user_id"]).to eq(admin.id)
end
end
context 'when email is incorrect' do
it 'should return the right response' do
put "/u/admin-login", params: { email: 'random' }
expect(response.status).to eq(200)
response_body = response.body
expect(response_body).to match(I18n.t("admin_login.errors.unknown_email_address"))
expect(response_body).to_not match(I18n.t("login.second_factor_description"))
end
end
end
describe '#toggle_anon' do
it 'allows you to toggle anon if enabled' do
SiteSetting.allow_anonymous_posting = true
user = sign_in(Fabricate(:user))
user.trust_level = 1
user.save!
post "/u/toggle-anon.json"
expect(response.status).to eq(200)
expect(session[:current_user_id]).to eq(AnonymousShadowCreator.get(user).id)
post "/u/toggle-anon.json"
expect(response.status).to eq(200)
expect(session[:current_user_id]).to eq(user.id)
end
end
describe '#create' do
def honeypot_magic(params)
get '/u/hp.json'
json = JSON.parse(response.body)
params[:password_confirmation] = json["value"]
params[:challenge] = json["challenge"].reverse
params
end
before do
UsersController.any_instance.stubs(:honeypot_value).returns(nil)
UsersController.any_instance.stubs(:challenge_value).returns(nil)
SiteSetting.allow_new_registrations = true
@user = Fabricate.build(:user, password: "strongpassword")
end
let(:post_user_params) do
{ name: @user.name,
username: @user.username,
password: "strongpassword",
email: @user.email }
end
def post_user
post "/u.json", params: post_user_params
end
context 'when email params is missing' do
it 'should raise the right error' do
post "/u.json", params: {
name: @user.name,
username: @user.username,
password: 'tesing12352343'
}
expect(response.status).to eq(400)
end
end
context 'when creating a user' do
it 'sets the user locale to I18n.locale' do
SiteSetting.default_locale = 'en'
I18n.stubs(:locale).returns(:fr)
post_user
expect(User.find_by(username: @user.username).locale).to eq('fr')
end
context "when timezone is provided as a guess on signup" do
let(:post_user_params) do
{ name: @user.name,
username: @user.username,
password: "strongpassword",
email: @user.email,
timezone: "Australia/Brisbane" }
end
it "sets the timezone" do
post_user
expect(response.status).to eq(200)
expect(User.find_by(username: @user.username).user_option.timezone).to eq("Australia/Brisbane")
end
end
end
context 'when creating a non active user (unconfirmed email)' do
it 'returns 403 forbidden when local logins are disabled' do
SiteSetting.enable_local_logins = false
post_user
expect(response.status).to eq(403)
end
it 'returns an error when new registrations are disabled' do
SiteSetting.allow_new_registrations = false
post_user
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json['success']).to eq(false)
expect(json['message']).to be_present
end
it 'creates a user correctly' do
post_user
expect(response.status).to eq(200)
expect(JSON.parse(response.body)['active']).to be_falsey
# should save user_created_message in session
expect(session["user_created_message"]).to be_present
expect(session[SessionController::ACTIVATE_USER_KEY]).to be_present
expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(1)
args = Jobs::CriticalUserEmail.jobs.first["args"].first
expect(args["type"]).to eq("signup")
end
context "`must approve users` site setting is enabled" do
before { SiteSetting.must_approve_users = true }
it 'creates a user correctly' do
post_user
expect(response.status).to eq(200)
expect(JSON.parse(response.body)['active']).to be_falsey
# should save user_created_message in session
expect(session["user_created_message"]).to be_present
expect(session[SessionController::ACTIVATE_USER_KEY]).to be_present
expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(1)
args = Jobs::CriticalUserEmail.jobs.first["args"].first
expect(args["type"]).to eq("signup")
end
end
context 'users already exists with given email' do
let!(:existing) { Fabricate(:user, email: post_user_params[:email]) }
it 'returns an error if hide_email_address_taken is disabled' do
SiteSetting.hide_email_address_taken = false
post_user
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json['success']).to eq(false)
expect(json['message']).to be_present
end
it 'returns success if hide_email_address_taken is enabled' do
SiteSetting.hide_email_address_taken = true
expect {
post_user
}.to_not change { User.count }
expect(response.status).to eq(200)
expect(session["user_created_message"]).to be_present
json = JSON.parse(response.body)
expect(json['active']).to be_falsey
expect(json['message']).to eq(I18n.t("login.activate_email", email: post_user_params[:email]))
end
end
end
context "creating as active" do
it "won't create the user as active" do
post "/u.json", params: post_user_params.merge(active: true)
expect(response.status).to eq(200)
expect(JSON.parse(response.body)['active']).to be_falsey
end
context "with a regular api key" do
fab!(:user) { Fabricate(:user) }
fab!(:api_key, refind: false) { Fabricate(:api_key, user: user) }
it "won't create the user as active with a regular key" do
post "/u.json",
params: post_user_params.merge(active: true, api_key: api_key.key)
expect(response.status).to eq(200)
expect(JSON.parse(response.body)['active']).to be_falsey
end
end
context "with an admin api key" do
fab!(:admin) { Fabricate(:admin) }
fab!(:api_key, refind: false) { Fabricate(:api_key, user: admin) }
it "creates the user as active with a an admin key" do
SiteSetting.send_welcome_message = true
SiteSetting.must_approve_users = true
#Sidekiq::Client.expects(:enqueue).never
post "/u.json", params: post_user_params.merge(approved: true, active: true, api_key: api_key.key)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
expect(response.status).to eq(200)
json = JSON.parse(response.body)
new_user = User.find(json["user_id"])
expect(json['active']).to be_truthy
expect(new_user.active).to eq(true)
expect(new_user.approved).to eq(true)
expect(new_user.approved_by_id).to eq(admin.id)
expect(new_user.approved_at).to_not eq(nil)
end
it "will create a reviewable when a user is created as active but not approved" do
Jobs.run_immediately!
SiteSetting.must_approve_users = true
post "/u.json", params: post_user_params.merge(active: true, api_key: api_key.key)
expect(response.status).to eq(200)
json = JSON.parse(response.body)
new_user = User.find(json["user_id"])
expect(json['active']).to be_truthy
expect(new_user.approved).to eq(false)
expect(ReviewableUser.pending.find_by(target: new_user)).to be_present
end
it "won't create a reviewable when a user is not active" do
Jobs.run_immediately!
SiteSetting.must_approve_users = true
post "/u.json", params: post_user_params.merge(api_key: api_key.key)
expect(response.status).to eq(200)
json = JSON.parse(response.body)
new_user = User.find(json["user_id"])
expect(json['active']).to eq(false)
expect(new_user.approved).to eq(false)
expect(ReviewableUser.pending.find_by(target: new_user)).to be_blank
end
it "won't create the developer as active" do
UsernameCheckerService.expects(:is_developer?).returns(true)
post "/u.json", params: post_user_params.merge(active: true, api_key: api_key.key)
expect(response.status).to eq(200)
expect(JSON.parse(response.body)['active']).to be_falsy
end
it "won't set the new user's locale to the admin's locale" do
SiteSetting.allow_user_locale = true
admin.update!(locale: :fr)
post "/u.json", params: post_user_params.merge(active: true, api_key: api_key.key)
expect(response.status).to eq(200)
json = JSON.parse(response.body)
new_user = User.find(json["user_id"])
expect(new_user.locale).not_to eq("fr")
end
end
end
context "creating as staged" do
it "won't create the user as staged" do
post "/u.json", params: post_user_params.merge(staged: true)
expect(response.status).to eq(200)
new_user = User.where(username: post_user_params[:username]).first
expect(new_user.staged?).to eq(false)
end
context "with a regular api key" do
fab!(:user) { Fabricate(:user) }
fab!(:api_key, refind: false) { Fabricate(:api_key, user: user) }
it "won't create the user as staged with a regular key" do
post "/u.json", params: post_user_params.merge(staged: true, api_key: api_key.key)
expect(response.status).to eq(200)
new_user = User.where(username: post_user_params[:username]).first
expect(new_user.staged?).to eq(false)
end
end
context "with an admin api key" do
fab!(:user) { Fabricate(:admin) }
fab!(:api_key, refind: false) { Fabricate(:api_key, user: user) }
it "creates the user as staged with a regular key" do
post "/u.json", params: post_user_params.merge(staged: true, api_key: api_key.key)
expect(response.status).to eq(200)
new_user = User.where(username: post_user_params[:username]).first
expect(new_user.staged?).to eq(true)
end
it "won't create the developer as staged" do
UsernameCheckerService.expects(:is_developer?).returns(true)
post "/u.json", params: post_user_params.merge(staged: true, api_key: api_key.key)
expect(response.status).to eq(200)
new_user = User.where(username: post_user_params[:username]).first
expect(new_user.staged?).to eq(false)
end
end
end
context 'when creating an active user (confirmed email)' do
before { User.any_instance.stubs(:active?).returns(true) }
it 'enqueues a welcome email' do
User.any_instance.expects(:enqueue_welcome_message).with('welcome_user')
post_user
expect(response.status).to eq(200)
# should save user_created_message in session
expect(session["user_created_message"]).to be_present
expect(session[SessionController::ACTIVATE_USER_KEY]).to be_present
end
it "shows the 'active' message" do
User.any_instance.expects(:enqueue_welcome_message)
post_user
expect(response.status).to eq(200)
expect(JSON.parse(response.body)['message']).to eq(
I18n.t 'login.active'
)
end
it "should be logged in" do
User.any_instance.expects(:enqueue_welcome_message)
post_user
expect(response.status).to eq(200)
expect(session[:current_user_id]).to be_present
end
it 'indicates the user is active in the response' do
User.any_instance.expects(:enqueue_welcome_message)
post_user
expect(response.status).to eq(200)
expect(JSON.parse(response.body)['active']).to be_truthy
end
it 'doesn\'t succeed when new registrations are disabled' do
SiteSetting.allow_new_registrations = false
post_user
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json['success']).to eq(false)
expect(json['message']).to be_present
end
context 'authentication records for' do
before do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:twitter] = OmniAuth::AuthHash.new(
provider: 'twitter',
uid: '123545',
info: OmniAuth::AuthHash::InfoHash.new(
email: "osama@mail.com",
nickname: "testosama"
)
)
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter]
SiteSetting.enable_twitter_logins = true
get "/auth/twitter/callback.json"
end
after do
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter] = nil
OmniAuth.config.test_mode = false
end
it 'should create twitter user info if required' do
post "/u.json", params: {
name: "Test Osama",
username: "testosama",
password: "strongpassword",
email: "osama@mail.com"
}
expect(response.status).to eq(200)
expect(UserAssociatedAccount.where(provider_name: "twitter").count).to eq(1)
end
it "returns an error when email has been changed from the validated email address" do
post "/u.json", params: {
name: "Test Osama",
username: "testosama",
password: "strongpassword",
email: "unvalidatedemail@mail.com"
}
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json['success']).to eq(false)
expect(json['message']).to be_present
end
it "will create the user successfully if email validation is required" do
post "/u.json", params: {
name: "Test Osama",
username: "testosama",
password: "strongpassword",
email: "osama@mail.com"
}
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json['success']).to eq(true)
end
end
end
it "creates user successfully but doesn't activate the account" do
post_user
expect(response.status).to eq(200)
json = JSON::parse(response.body)
expect(json["success"]).to eq(true)
expect(User.find_by(username: @user.username).active).to eq(false)
end
shared_examples 'honeypot fails' do
it 'should not create a new user' do
User.any_instance.expects(:enqueue_welcome_message).never
expect {
post "/u.json", params: create_params
}.to_not change { User.count }
expect(response.status).to eq(200)
json = JSON::parse(response.body)
expect(json["success"]).to eq(true)
# should not change the session
expect(session["user_created_message"]).to be_blank
expect(session[SessionController::ACTIVATE_USER_KEY]).to be_blank
end
end
context 'when honeypot value is wrong' do
before do
UsersController.any_instance.stubs(:honeypot_value).returns('abc')
end
let(:create_params) { { name: @user.name, username: @user.username, password: "strongpassword", email: @user.email, password_confirmation: 'wrong' } }
include_examples 'honeypot fails'
end
context 'when challenge answer is wrong' do
before do
UsersController.any_instance.stubs(:challenge_value).returns('abc')
end
let(:create_params) { { name: @user.name, username: @user.username, password: "strongpassword", email: @user.email, challenge: 'abc' } }
include_examples 'honeypot fails'
end
context "when 'invite only' setting is enabled" do
before { SiteSetting.invite_only = true }
let(:create_params) { {
name: @user.name,
username: @user.username,
password: 'strongpassword',
email: @user.email
}}
include_examples 'honeypot fails'
end
shared_examples 'failed signup' do
it 'should not create a new User' do
expect { post "/u.json", params: create_params }.to_not change { User.count }
expect(response.status).to eq(200)
end
it 'should report failed' do
post "/u.json", params: create_params
json = JSON::parse(response.body)
expect(json["success"]).not_to eq(true)
# should not change the session
expect(session["user_created_message"]).to be_blank
expect(session[SessionController::ACTIVATE_USER_KEY]).to be_blank
end
end
context 'when password is blank' do
let(:create_params) { { name: @user.name, username: @user.username, password: "", email: @user.email } }
include_examples 'failed signup'
end
context 'when password is too long' do
let(:create_params) { { name: @user.name, username: @user.username, password: "x" * (User.max_password_length + 1), email: @user.email } }
include_examples 'failed signup'
end
context 'when password param is missing' do
let(:create_params) { { name: @user.name, username: @user.username, email: @user.email } }
include_examples 'failed signup'
end
context 'with a reserved username' do
let(:create_params) { { name: @user.name, username: 'Reserved', email: @user.email, password: 'strongpassword' } }
before { SiteSetting.reserved_usernames = 'a|reserved|b' }
include_examples 'failed signup'
end
context 'with a username that matches a user route' do
let(:create_params) { { name: @user.name, username: 'account-created', email: @user.email, password: 'strongpassword' } }
include_examples 'failed signup'
end
context 'with a missing username' do
let(:create_params) { { name: @user.name, email: @user.email, password: "x" * 20 } }
it 'should not create a new User' do
expect { post "/u.json", params: create_params }.to_not change { User.count }
expect(response.status).to eq(400)
end
end
context 'when an Exception is raised' do
before { User.any_instance.stubs(:save).raises(ActiveRecord::StatementInvalid.new('Oh no')) }
let(:create_params) {
{ name: @user.name, username: @user.username,
password: "strongpassword", email: @user.email }
}
include_examples 'failed signup'
end
context "with custom fields" do
let!(:user_field) { Fabricate(:user_field) }
let!(:another_field) { Fabricate(:user_field) }
let!(:optional_field) { Fabricate(:user_field, required: false) }
context "without a value for the fields" do
let(:create_params) { { name: @user.name, password: 'watwatwat', username: @user.username, email: @user.email } }
include_examples 'failed signup'
end
context "with values for the fields" do
let(:create_params) { {
name: @user.name,
password: 'suChS3cuRi7y',
username: @user.username,
email: @user.email,
user_fields: {
user_field.id.to_s => 'value1',
another_field.id.to_s => 'value2',
}
} }
it "should succeed without the optional field" do
post "/u.json", params: create_params
expect(response.status).to eq(200)
inserted = User.find_by_email(@user.email)
expect(inserted).to be_present
expect(inserted.custom_fields).to be_present
expect(inserted.custom_fields["user_field_#{user_field.id}"]).to eq('value1')
expect(inserted.custom_fields["user_field_#{another_field.id}"]).to eq('value2')
expect(inserted.custom_fields["user_field_#{optional_field.id}"]).to be_blank
end
it "should succeed with the optional field" do
create_params[:user_fields][optional_field.id.to_s] = 'value3'
post "/u.json", params: create_params.merge(create_params)
expect(response.status).to eq(200)
inserted = User.find_by_email(@user.email)
expect(inserted).to be_present
expect(inserted.custom_fields).to be_present
expect(inserted.custom_fields["user_field_#{user_field.id}"]).to eq('value1')
expect(inserted.custom_fields["user_field_#{another_field.id}"]).to eq('value2')
expect(inserted.custom_fields["user_field_#{optional_field.id}"]).to eq('value3')
end
it "trims excessively long fields" do
create_params[:user_fields][optional_field.id.to_s] = ('x' * 3000)
post "/u.json", params: create_params.merge(create_params)
expect(response.status).to eq(200)
inserted = User.find_by_email(@user.email)
val = inserted.custom_fields["user_field_#{optional_field.id}"]
expect(val.length).to eq(UserField.max_length)
end
end
end
context "with only optional custom fields" do
let!(:user_field) { Fabricate(:user_field, required: false) }
context "without values for the fields" do
let(:create_params) { {
name: @user.name,
password: 'suChS3cuRi7y',
username: @user.username,
email: @user.email,
} }
it "should succeed" do
post "/u.json", params: create_params
expect(response.status).to eq(200)
inserted = User.find_by_email(@user.email)
expect(inserted).to be_present
expect(inserted.custom_fields).not_to be_present
expect(inserted.custom_fields["user_field_#{user_field.id}"]).to be_blank
end
end
end
context "when taking over a staged account" do
before do
UsersController.any_instance.stubs(:honeypot_value).returns("abc")
UsersController.any_instance.stubs(:challenge_value).returns("efg")
end
let!(:staged) { Fabricate(:staged, email: "staged@account.com", active: true) }
it "succeeds" do
post '/u.json', params: honeypot_magic(
email: staged.email,
username: "zogstrip",
password: "P4ssw0rd$$"
)
expect(response.status).to eq(200)
result = ::JSON.parse(response.body)
expect(result["success"]).to eq(true)
created_user = User.find_by_email(staged.email)
expect(created_user.staged).to eq(false)
expect(created_user.active).to eq(false)
expect(created_user.registration_ip_address).to be_present
expect(!!created_user.custom_fields["from_staged"]).to eq(true)
# do not allow emails changes please
put "/u/update-activation-email.json", params: { email: 'bob@bob.com' }
created_user.reload
expect(created_user.email).to eq("staged@account.com")
expect(response.status).to eq(403)
end
end
end
describe '#username' do
it 'raises an error when not logged in' do
put "/u/somename/preferences/username.json"
expect(response.status).to eq(403)
end
context 'while logged in' do
let(:old_username) { "OrigUsrname" }
let(:new_username) { "#{old_username}1234" }
let(:user) { Fabricate(:user, username: old_username) }
before do
user.username = old_username
sign_in(user)
end
it 'raises an error without a new_username param' do
put "/u/#{user.username}/preferences/username.json", params: { username: user.username }
expect(response.status).to eq(400)
expect(user.reload.username).to eq(old_username)
end
it 'raises an error when you don\'t have permission to change the username' do
Guardian.any_instance.expects(:can_edit_username?).with(user).returns(false)
put "/u/#{user.username}/preferences/username.json", params: { new_username: new_username }
expect(response).to be_forbidden
expect(user.reload.username).to eq(old_username)
end
it 'raises an error when change_username fails' do
put "/u/#{user.username}/preferences/username.json", params: { new_username: '@' }
expect(response.status).to eq(422)
body = JSON.parse(response.body)
expect(body['errors'].first).to include(I18n.t(
'user.username.short', min: User.username_length.begin
))
expect(user.reload.username).to eq(old_username)
end
it 'should succeed in normal circumstances' do
put "/u/#{user.username}/preferences/username.json", params: { new_username: new_username }
expect(response.status).to eq(200)
expect(user.reload.username).to eq(new_username)
end
it 'raises an error when the username clashes with an existing user route' do
put "/u/#{user.username}/preferences/username.json", params: { new_username: 'account-created' }
body = JSON.parse(response.body)
expect(body['errors'].first).to include(I18n.t('login.reserved_username'))
end
it 'raises an error when the username is in the reserved list' do
SiteSetting.reserved_usernames = 'reserved'
put "/u/#{user.username}/preferences/username.json", params: { new_username: 'reserved' }
body = JSON.parse(response.body)
expect(body['errors'].first).to include(I18n.t('login.reserved_username'))
end
it 'should fail if the user is old' do
# Older than the change period and >1 post
user.created_at = Time.now - (SiteSetting.username_change_period + 1).days
PostCreator.new(user,
title: 'This is a test topic',
raw: 'This is a test this is a test'
).create
put "/u/#{user.username}/preferences/username.json", params: { new_username: new_username }
expect(response).to be_forbidden
expect(user.reload.username).to eq(old_username)
end
it 'should create a staff action log when a staff member changes the username' do
acting_user = Fabricate(:admin)
sign_in(acting_user)
put "/u/#{user.username}/preferences/username.json", params: { new_username: new_username }
expect(response.status).to eq(200)
expect(UserHistory.where(action: UserHistory.actions[:change_username], target_user_id: user.id, acting_user_id: acting_user.id)).to be_present
expect(user.reload.username).to eq(new_username)
end
it 'should return a JSON response with the updated username' do
put "/u/#{user.username}/preferences/username.json", params: { new_username: new_username }
expect(::JSON.parse(response.body)['username']).to eq(new_username)
end
it 'should respond with proper error message if sso_overrides_username is enabled' do
SiteSetting.sso_url = 'http://someurl.com'
SiteSetting.enable_sso = true
SiteSetting.sso_overrides_username = true
acting_user = Fabricate(:admin)
sign_in(acting_user)
put "/u/#{user.username}/preferences/username.json", params: { new_username: new_username }
expect(response.status).to eq(422)
expect(::JSON.parse(response.body)['errors'].first).to include(I18n.t('errors.messages.sso_overrides_username'))
end
end
end
describe '#check_username' do
it 'raises an error without any parameters' do
get "/u/check_username.json"
expect(response.status).to eq(400)
end
shared_examples 'when username is unavailable' do
it 'should return available as false in the JSON and return a suggested username' do
expect(response.status).to eq(200)
expect(::JSON.parse(response.body)['available']).to eq(false)
expect(::JSON.parse(response.body)['suggestion']).to be_present
end
end
shared_examples 'when username is available' do
it 'should return available in the JSON' do
expect(response.status).to eq(200)
expect(::JSON.parse(response.body)['available']).to eq(true)
end
end
it 'returns nothing when given an email param but no username' do
get "/u/check_username.json", params: { email: 'dood@example.com' }
expect(response.status).to eq(200)
end
context 'username is available' do
before do
get "/u/check_username.json", params: { username: 'BruceWayne' }
end
include_examples 'when username is available'
end
context 'username is unavailable' do
let!(:user) { Fabricate(:user) }
before do
get "/u/check_username.json", params: { username: user.username }
end
include_examples 'when username is unavailable'
end
shared_examples 'checking an invalid username' do
it 'should not return an available key but should return an error message' do
expect(response.status).to eq(200)
expect(::JSON.parse(response.body)['available']).to eq(nil)
expect(::JSON.parse(response.body)['errors']).to be_present
end
end
context 'has invalid characters' do
before do
get "/u/check_username.json", params: { username: 'bad username' }
end
include_examples 'checking an invalid username'
it 'should return the invalid characters message' do
expect(response.status).to eq(200)
expect(::JSON.parse(response.body)['errors']).to include(I18n.t(:'user.username.characters'))
end
end
context 'is too long' do
before do
get "/u/check_username.json", params: { username: generate_username(User.username_length.last + 1) }
end
include_examples 'checking an invalid username'
it 'should return the "too long" message' do
expect(response.status).to eq(200)
expect(::JSON.parse(response.body)['errors']).to include(I18n.t(:'user.username.long', max: User.username_length.end))
end
end
describe 'different case of existing username' do
context "it's my username" do
let!(:user) { Fabricate(:user, username: 'hansolo') }
before do
sign_in(user)
get "/u/check_username.json", params: { username: 'HanSolo' }
end
include_examples 'when username is available'
end
context "it's someone else's username" do
let!(:user) { Fabricate(:user, username: 'hansolo') }
before do
sign_in(Fabricate(:user))
get "/u/check_username.json", params: { username: 'HanSolo' }
end
include_examples 'when username is unavailable'
end
context "an admin changing it for someone else" do
let!(:user) { Fabricate(:user, username: 'hansolo') }
before do
sign_in(Fabricate(:admin))
get "/u/check_username.json", params: { username: 'HanSolo', for_user_id: user.id }
end
include_examples 'when username is available'
end
end
end
describe "#invited_count" do
it "fails for anonymous users" do
user = Fabricate(:user)
get "/u/#{user.username}/invited_count.json"
expect(response.status).to eq(403)
end
it "works for users who can see invites" do
inviter = Fabricate(:user, trust_level: 2)
sign_in(inviter)
invitee = Fabricate(:user)
invite = Fabricate(:invite, invited_by: inviter, user: invitee)
get "/u/#{user.username}/invited_count.json"
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json).to be_present
expect(json['counts']).to be_present
end
end
describe '#invited' do
it 'fails for anonymous users' do
user = Fabricate(:user)
get "/u/#{user.username}/invited.json", params: { username: user.username }
expect(response.status).to eq(403)
end
it 'returns success' do
user = Fabricate(:user, trust_level: 2)
sign_in(user)
get "/u/#{user.username}/invited.json", params: { username: user.username }
expect(response.status).to eq(200)
end
it 'filters by all if viewing self' do
inviter = Fabricate(:user, trust_level: 2)
sign_in(inviter)
invitee = Fabricate(:user)
Fabricate(:invite, email: 'billybob@example.com', invited_by: inviter, user: invitee)
Fabricate(:invite, email: 'jimtom@example.com', invited_by: inviter, user: invitee)
get "/u/#{inviter.username}/invited.json", params: { search: 'billybob' }
expect(response.status).to eq(200)
invites = JSON.parse(response.body)['invites']
expect(invites.size).to eq(1)
expect(invites.first).to include('email' => 'billybob@example.com')
get "/u/#{inviter.username}/invited.json", params: { search: invitee.username }
expect(response.status).to eq(200)
invites = JSON.parse(response.body)['invites']
expect(invites.size).to eq(2)
expect(invites[0]['email']).to be_present
end
it "doesn't filter by email if another regular user" do
inviter = Fabricate(:user, trust_level: 2)
sign_in(Fabricate(:user, trust_level: 2))
invitee = Fabricate(:user)
Fabricate(:invite, email: 'billybob@example.com', invited_by: inviter, user: invitee)
Fabricate(:invite, email: 'jimtom@example.com', invited_by: inviter, user: invitee)
get "/u/#{inviter.username}/invited.json", params: { search: 'billybob' }
expect(response.status).to eq(200)
invites = JSON.parse(response.body)['invites']
expect(invites.size).to eq(0)
get "/u/#{inviter.username}/invited.json", params: { search: invitee.username }
expect(response.status).to eq(200)
invites = JSON.parse(response.body)['invites']
expect(invites.size).to eq(2)
expect(invites[0]['email']).to be_blank
end
it "filters by email if staff" do
inviter = Fabricate(:user, trust_level: 2)
sign_in(Fabricate(:moderator))
invitee = Fabricate(:user)
Fabricate(:invite, email: 'billybob@example.com', invited_by: inviter, user: invitee)
Fabricate(:invite, email: 'jimtom@example.com', invited_by: inviter, user: invitee)
get "/u/#{inviter.username}/invited.json", params: { search: 'billybob' }
expect(response.status).to eq(200)
invites = JSON.parse(response.body)['invites']
expect(invites.size).to eq(1)
expect(invites[0]['email']).to be_present
end
context 'with guest' do
context 'with pending invites' do
it 'does not return invites' do
inviter = Fabricate(:user)
Fabricate(:invite, invited_by: inviter)
get "/u/#{user.username}/invited/pending.json"
expect(response.status).to eq(403)
end
end
context 'with redeemed invites' do
it 'returns invites' do
inviter = Fabricate(:user, trust_level: 2)
sign_in(inviter)
invitee = Fabricate(:user)
invite = Fabricate(:invite, invited_by: inviter, user: invitee)
get "/u/#{inviter.username}/invited.json"
expect(response.status).to eq(200)
invites = JSON.parse(response.body)['invites']
expect(invites.size).to eq(1)
expect(invites.first).to include('email' => invite.email)
end
end
end
context 'with authenticated user' do
context 'with pending invites' do
context 'with permission to see pending invites' do
it 'returns invites' do
inviter = Fabricate(:user, trust_level: 2)
invite = Fabricate(:invite, invited_by: inviter)
sign_in(inviter)
get "/u/#{inviter.username}/invited/pending.json"
expect(response.status).to eq(200)
invites = JSON.parse(response.body)['invites']
expect(invites.size).to eq(1)
expect(invites.first).to include("email" => invite.email)
end
end
context 'without permission to see pending invites' do
it 'does not return invites' do
user = sign_in(Fabricate(:user))
inviter = Fabricate(:user)
_invitee = Fabricate(:user)
Fabricate(:invite, invited_by: inviter)
stub_guardian(user) do |guardian|
guardian.stubs(:can_see_invite_details?).
with(inviter).returns(false)
end
get "/u/#{inviter.username}/invited/pending.json"
expect(response.status).to eq(403)
end
end
end
context 'with redeemed invites' do
it 'returns invites' do
sign_in(Fabricate(:moderator))
inviter = Fabricate(:user)
invitee = Fabricate(:user)
invite = Fabricate(:invite, invited_by: inviter, user: invitee)
get "/u/#{inviter.username}/invited.json"
expect(response.status).to eq(200)
invites = JSON.parse(response.body)['invites']
expect(invites.size).to eq(1)
expect(invites.first).to include('email' => invite.email)
end
end
end
end
describe '#update' do
context 'with guest' do
it 'raises an error' do
put "/u/guest.json"
expect(response.status).to eq(403)
end
end
context "when username contains a period" do
before do
sign_in(user)
end
fab!(:user) { Fabricate(:user, username: 'test.test', name: "Test User") }
it "should be able to update a user" do
put "/u/#{user.username}", params: { name: 'test.test' }
expect(response.status).to eq(200)
expect(user.reload.name).to eq('test.test')
end
end
context "as a staff user" do
context "uneditable field" do
let!(:user_field) { Fabricate(:user_field, editable: false) }
it "allows staff to edit the field" do
sign_in(Fabricate(:admin))
user = Fabricate(:user)
put "/u/#{user.username}.json", params: {
name: 'Jim Tom',
title: "foobar",
user_fields: { user_field.id.to_s => 'happy' }
}
expect(response.status).to eq(200)
user.reload
expect(user.user_fields[user_field.id.to_s]).to eq('happy')
expect(user.title).to eq("foobar")
end
end
end
context 'with authenticated user' do
context 'with permission to update' do
let(:upload) { Fabricate(:upload) }
let!(:user) { sign_in(Fabricate(:user)) }
it 'allows the update' do
SiteSetting.tagging_enabled = true
user2 = Fabricate(:user)
user3 = Fabricate(:user)
tags = [Fabricate(:tag), Fabricate(:tag)]
tag_synonym = Fabricate(:tag, target_tag: tags[1])
put "/u/#{user.username}.json", params: {
name: 'Jim Tom',
muted_usernames: "#{user2.username},#{user3.username}",
watched_tags: "#{tags[0].name},#{tag_synonym.name}",
card_background_upload_url: upload.url,
profile_background_upload_url: upload.url
}
expect(response.status).to eq(200)
user.reload
expect(user.name).to eq 'Jim Tom'
expect(user.muted_users.pluck(:username).sort).to eq [user2.username, user3.username].sort
expect(TagUser.where(
user: user,
notification_level: TagUser.notification_levels[:watching]
).pluck(:tag_id)).to contain_exactly(tags[0].id, tags[1].id)
theme = Fabricate(:theme, user_selectable: true)
put "/u/#{user.username}.json", params: {
muted_usernames: "",
theme_ids: [theme.id],
email_level: UserOption.email_level_types[:always]
}
user.reload
expect(user.muted_users.pluck(:username).sort).to be_empty
expect(user.user_option.theme_ids).to eq([theme.id])
expect(user.user_option.email_level).to eq(UserOption.email_level_types[:always])
expect(user.profile_background_upload).to eq(upload)
expect(user.card_background_upload).to eq(upload)
end
context 'a locale is chosen that differs from I18n.locale' do
before do
SiteSetting.allow_user_locale = true
end
it "updates the user's locale" do
I18n.locale = :fr
put "/u/#{user.username}.json", params: { locale: :fa_IR }
expect(user.reload.locale).to eq('fa_IR')
end
it "updates the title" do
user.update!(locale: :fr)
user.change_trust_level!(TrustLevel[4])
BadgeGranter.process_queue!
leader_title = I18n.t("badges.leader.name", locale: :fr)
put "/u/#{user.username}.json", params: { title: leader_title }
expect(user.reload.title).to eq(leader_title)
end
end
context "with user fields" do
context "an editable field" do
let!(:user_field) { Fabricate(:user_field) }
let!(:optional_field) { Fabricate(:user_field, required: false) }
it "should update the user field" do
put "/u/#{user.username}.json", params: { name: 'Jim Tom', user_fields: { user_field.id.to_s => 'happy' } }
expect(response.status).to eq(200)
expect(user.user_fields[user_field.id.to_s]).to eq 'happy'
end
it "cannot be updated to blank" do
put "/u/#{user.username}.json", params: { name: 'Jim Tom', user_fields: { user_field.id.to_s => '' } }
expect(response.status).to eq(422)
expect(user.user_fields[user_field.id.to_s]).not_to eq('happy')
end
it "trims excessively large fields" do
put "/u/#{user.username}.json", params: { name: 'Jim Tom', user_fields: { user_field.id.to_s => ('x' * 3000) } }
expect(user.user_fields[user_field.id.to_s].size).to eq(UserField.max_length)
end
it "should retain existing user fields" do
put "/u/#{user.username}.json", params: { name: 'Jim Tom', user_fields: { user_field.id.to_s => 'happy', optional_field.id.to_s => 'feet' } }
expect(response.status).to eq(200)
expect(user.user_fields[user_field.id.to_s]).to eq('happy')
expect(user.user_fields[optional_field.id.to_s]).to eq('feet')
put "/u/#{user.username}.json", params: { name: 'Jim Tom', user_fields: { user_field.id.to_s => 'sad' } }
expect(response.status).to eq(200)
user.reload
expect(user.user_fields[user_field.id.to_s]).to eq('sad')
expect(user.user_fields[optional_field.id.to_s]).to eq('feet')
end
end
context "uneditable field" do
let!(:user_field) { Fabricate(:user_field, editable: false) }
it "does not update the user field" do
put "/u/#{user.username}.json", params: { name: 'Jim Tom', user_fields: { user_field.id.to_s => 'happy' } }
expect(response.status).to eq(200)
expect(user.user_fields[user_field.id.to_s]).to be_blank
end
end
context "custom_field" do
before do
plugin = Plugin::Instance.new
plugin.register_editable_user_custom_field :test2
plugin.register_editable_user_custom_field :test3, staff_only: true
end
after do
User.plugin_editable_user_custom_fields.clear
User.plugin_staff_editable_user_custom_fields.clear
end
it "only updates allowed user fields" do
put "/u/#{user.username}.json", params: { custom_fields: { test1: :hello1, test2: :hello2, test3: :hello3 } }
expect(response.status).to eq(200)
expect(user.custom_fields["test1"]).to be_blank
expect(user.custom_fields["test2"]).to eq("hello2")
expect(user.custom_fields["test3"]).to be_blank
end
it "works alongside a user field" do
user_field = Fabricate(:user_field, editable: true)
put "/u/#{user.username}.json", params: { custom_fields: { test1: :hello1, test2: :hello2, test3: :hello3 }, user_fields: { user_field.id.to_s => 'happy' } }
expect(response.status).to eq(200)
expect(user.custom_fields["test1"]).to be_blank
expect(user.custom_fields["test2"]).to eq("hello2")
expect(user.custom_fields["test3"]).to eq(nil)
expect(user.user_fields[user_field.id.to_s]).to eq('happy')
end
it "works alongside a user field during creation" do
api_key = Fabricate(:api_key, user: Fabricate(:admin))
user_field = Fabricate(:user_field, editable: true)
post "/u.json", params: {
name: "Test User",
username: "testuser",
email: "user@mail.com",
password: 'supersecure',
active: true,
custom_fields: {
test2: 'custom field value'
},
user_fields: {
user_field.id.to_s => 'user field value'
},
api_key: api_key.key
}
expect(response.status).to eq(200)
u = User.find_by_email('user@mail.com')
val = u.custom_fields["user_field_#{user_field.id}"]
expect(val).to eq('user field value')
val = u.custom_fields["test2"]
expect(val).to eq('custom field value')
end
it "is secure when there are no registered editable fields" do
User.plugin_editable_user_custom_fields.clear
User.plugin_staff_editable_user_custom_fields.clear
put "/u/#{user.username}.json", params: { custom_fields: { test1: :hello1, test2: :hello2, test3: :hello3 } }
expect(response.status).to eq(200)
expect(user.custom_fields["test1"]).to be_blank
expect(user.custom_fields["test2"]).to be_blank
expect(user.custom_fields["test3"]).to be_blank
put "/u/#{user.username}.json", params: { custom_fields: ["arrayitem1", "arrayitem2"] }
expect(response.status).to eq(200)
end
it "allows staff to edit staff-editable fields" do
sign_in(Fabricate(:admin))
put "/u/#{user.username}.json", params: { custom_fields: { test1: :hello1, test2: :hello2, test3: :hello3 } }
expect(response.status).to eq(200)
expect(user.custom_fields["test1"]).to be_blank
expect(user.custom_fields["test2"]).to eq("hello2")
expect(user.custom_fields["test3"]).to eq("hello3")
end
end
end
it 'returns user JSON' do
put "/u/#{user.username}.json"
json = JSON.parse(response.body)
expect(json['user']['id']).to eq user.id
end
end
context 'without permission to update' do
it 'does not allow the update' do
user = Fabricate(:user, name: 'Billy Bob')
sign_in(Fabricate(:user))
put "/u/#{user.username}.json", params: { name: 'Jim Tom' }
expect(response).to be_forbidden
expect(user.reload.name).not_to eq 'Jim Tom'
end
end
end
end
describe '#badge_title' do
fab!(:user) { Fabricate(:user) }
fab!(:badge) { Fabricate(:badge) }
let(:user_badge) { BadgeGranter.grant(badge, user) }
it "sets the user's title to the badge name if it is titleable" do
sign_in(user)
put "/u/#{user.username}/preferences/badge_title.json", params: { user_badge_id: user_badge.id }
expect(user.reload.title).not_to eq(badge.display_name)
badge.update allow_title: true
put "/u/#{user.username}/preferences/badge_title.json", params: { user_badge_id: user_badge.id }
expect(user.reload.title).to eq(badge.display_name)
expect(user.user_profile.badge_granted_title).to eq(true)
expect(user.user_profile.granted_title_badge_id).to eq(badge.id)
badge.update allow_title: false
put "/u/#{user.username}/preferences/badge_title.json", params: { user_badge_id: user_badge.id }
user.reload
user.user_profile.reload
expect(user.title).to eq('')
expect(user.user_profile.badge_granted_title).to eq(false)
expect(user.user_profile.granted_title_badge_id).to eq(nil)
end
it "is not raising an erroring when user revokes title" do
sign_in(user)
badge.update allow_title: true
put "/u/#{user.username}/preferences/badge_title.json", params: { user_badge_id: user_badge.id }
put "/u/#{user.username}/preferences/badge_title.json", params: { user_badge_id: 0 }
expect(response.status).to eq(200)
end
context "with overridden name" do
fab!(:badge) { Fabricate(:badge, name: 'Demogorgon', allow_title: true) }
let(:user_badge) { BadgeGranter.grant(badge, user) }
before do
TranslationOverride.upsert!('en', 'badges.demogorgon.name', 'Boss')
end
after do
TranslationOverride.revert!('en', ['badges.demogorgon.name'])
end
it "uses the badge display name as user title" do
sign_in(user)
put "/u/#{user.username}/preferences/badge_title.json", params: { user_badge_id: user_badge.id }
expect(user.reload.title).to eq(badge.display_name)
end
end
end
describe '#send_activation_email' do
before do
UsersController.any_instance.stubs(:honeypot_value).returns(nil)
UsersController.any_instance.stubs(:challenge_value).returns(nil)
end
let(:post_user) do
post "/u.json", params: {
username: "osamatest",
password: "strongpassword",
email: "dsdsds@sasa.com"
}
User.find_by(username: "osamatest")
end
context 'for an existing user' do
context 'for an activated account with email confirmed' do
it 'fails' do
user = post_user
email_token = user.email_tokens.create(email: user.email).token
EmailToken.confirm(email_token)
post "/u/action/send_activation_email.json", params: { username: user.username }
expect(response.status).to eq(409)
expect(JSON.parse(response.body)['errors']).to include(I18n.t(
'activation.activated'
))
expect(session[SessionController::ACTIVATE_USER_KEY]).to eq(nil)
end
end
context 'for an activated account with unconfirmed email' do
it 'should send an email' do
user = post_user
user.update(active: true)
user.save!
user.email_tokens.create(email: user.email)
Jobs.expects(:enqueue).with(:critical_user_email, has_entries(type: :signup, to_address: user.email))
post "/u/action/send_activation_email.json", params: {
username: user.username
}
expect(response.status).to eq(200)
expect(session[SessionController::ACTIVATE_USER_KEY]).to eq(nil)
end
end
context "approval is enabled" do
before do
SiteSetting.must_approve_users = true
end
it "should raise an error" do
user = post_user
user.update(active: true)
user.save!
user.email_tokens.create(email: user.email)
post "/u/action/send_activation_email.json", params: {
username: user.username
}
expect(response.status).to eq(403)
end
end
describe 'when user does not have a valid session' do
it 'should not be valid' do
user = Fabricate(:user)
post "/u/action/send_activation_email.json", params: {
username: user.username
}
expect(response.status).to eq(403)
end
it 'should allow staff regardless' do
sign_in(Fabricate(:admin))
user = Fabricate(:user, active: false)
post "/u/action/send_activation_email.json", params: {
username: user.username
}
expect(response.status).to eq(200)
end
end
context 'with a valid email_token' do
it 'should send the activation email' do
user = post_user
Jobs.expects(:enqueue).with(:critical_user_email, has_entries(type: :signup))
post "/u/action/send_activation_email.json", params: {
username: user.username
}
expect(session[SessionController::ACTIVATE_USER_KEY]).to eq(nil)
end
end
context 'without an existing email_token' do
let(:user) { post_user }
before do
user.email_tokens.each { |t| t.destroy }
user.reload
end
it 'should generate a new token' do
expect {
post "/u/action/send_activation_email.json", params: { username: user.username }
}.to change { user.reload.email_tokens.count }.by(1)
end
it 'should send an email' do
expect do
post "/u/action/send_activation_email.json", params: {
username: user.username
}
end.to change { Jobs::CriticalUserEmail.jobs.size }.by(1)
expect(session[SessionController::ACTIVATE_USER_KEY]).to eq(nil)
end
end
end
context 'when username does not exist' do
it 'should not send an email' do
post "/u/action/send_activation_email.json", params: { username: 'nopenopenopenope' }
expect(response.status).to eq(404)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
end
end
end
describe '#pick_avatar' do
it 'raises an error when not logged in' do
put "/u/asdf/preferences/avatar/pick.json", params: { avatar_id: 1, type: "custom" }
expect(response.status).to eq(403)
end
context 'while logged in' do
before do
sign_in(user)
end
let(:upload) do
Fabricate(:upload, user: user)
end
it "raises an error when you don't have permission to toggle the avatar" do
another_user = Fabricate(:user)
put "/u/#{another_user.username}/preferences/avatar/pick.json", params: {
upload_id: upload.id, type: "custom"
}
expect(response).to be_forbidden
end
it "raises an error when sso_overrides_avatar is disabled" do
SiteSetting.sso_overrides_avatar = true
put "/u/#{user.username}/preferences/avatar/pick.json", params: {
upload_id: upload.id, type: "custom"
}
expect(response.status).to eq(422)
end
it "raises an error when selecting the custom/uploaded avatar and allow_uploaded_avatars is disabled" do
SiteSetting.allow_uploaded_avatars = false
put "/u/#{user.username}/preferences/avatar/pick.json", params: {
upload_id: upload.id, type: "custom"
}
expect(response.status).to eq(422)
end
it 'can successfully pick the system avatar' do
put "/u/#{user.username}/preferences/avatar/pick.json"
expect(response.status).to eq(200)
expect(user.reload.uploaded_avatar_id).to eq(nil)
end
it 'can successfully pick a gravatar' do
user.user_avatar.update_columns(gravatar_upload_id: upload.id)
put "/u/#{user.username}/preferences/avatar/pick.json", params: {
upload_id: upload.id, type: "gravatar"
}
expect(response.status).to eq(200)
expect(user.reload.uploaded_avatar_id).to eq(upload.id)
expect(user.user_avatar.reload.gravatar_upload_id).to eq(upload.id)
end
it 'can not pick uploads that were not created by user' do
upload2 = Fabricate(:upload)
put "/u/#{user.username}/preferences/avatar/pick.json", params: {
upload_id: upload2.id, type: "custom"
}
expect(response.status).to eq(403)
end
it 'can successfully pick a custom avatar' do
events = DiscourseEvent.track_events do
put "/u/#{user.username}/preferences/avatar/pick.json", params: {
upload_id: upload.id, type: "custom"
}
end
expect(events.map { |event| event[:event_name] }).to include(:user_updated)
expect(response.status).to eq(200)
expect(user.reload.uploaded_avatar_id).to eq(upload.id)
expect(user.user_avatar.reload.custom_upload_id).to eq(upload.id)
end
end
end
describe '#select_avatar' do
it 'raises an error when not logged in' do
put "/u/asdf/preferences/avatar/select.json", params: { url: "https://meta.discourse.org" }
expect(response.status).to eq(403)
end
context 'while logged in' do
let!(:user) { sign_in(Fabricate(:user)) }
fab!(:avatar1) { Fabricate(:upload) }
fab!(:avatar2) { Fabricate(:upload) }
let(:url) { "https://www.discourse.org" }
it 'raises an error when url is blank' do
put "/u/#{user.username}/preferences/avatar/select.json", params: { url: "" }
expect(response.status).to eq(422)
end
it 'raises an error when selectable avatars is disabled' do
put "/u/#{user.username}/preferences/avatar/select.json", params: { url: url }
expect(response.status).to eq(422)
end
context 'selectable avatars is enabled' do
before { SiteSetting.selectable_avatars_enabled = true }
it 'raises an error when selectable avatars is empty' do
put "/u/#{user.username}/preferences/avatar/select.json", params: { url: url }
expect(response.status).to eq(422)
end
context 'selectable avatars is properly setup' do
before do
SiteSetting.selectable_avatars = [avatar1.url, avatar2.url].join("\n")
end
it 'raises an error when url is not in selectable avatars list' do
put "/u/#{user.username}/preferences/avatar/select.json", params: { url: url }
expect(response.status).to eq(422)
end
it 'can successfully select an avatar' do
events = DiscourseEvent.track_events do
put "/u/#{user.username}/preferences/avatar/select.json", params: { url: avatar1.url }
end
expect(events.map { |event| event[:event_name] }).to include(:user_updated)
expect(response.status).to eq(200)
expect(user.reload.uploaded_avatar_id).to eq(avatar1.id)
expect(user.user_avatar.reload.custom_upload_id).to eq(avatar1.id)
end
end
end
end
end
describe '#destroy_user_image' do
it 'raises an error when not logged in' do
delete "/u/asdf/preferences/user_image.json", params: { type: 'profile_background' }
expect(response.status).to eq(403)
end
context 'while logged in' do
fab!(:another_user) { Fabricate(:user) }
fab!(:user) { Fabricate(:user) }
before do
sign_in(user)
end
it 'raises an error when you don\'t have permission to clear the profile background' do
delete "/u/#{another_user.username}/preferences/user_image.json", params: { type: 'profile_background' }
expect(response).to be_forbidden
end
it "requires the `type` param" do
delete "/u/#{user.username}/preferences/user_image.json"
expect(response.status).to eq(400)
end
it "only allows certain `types`" do
delete "/u/#{user.username}/preferences/user_image.json", params: { type: 'wat' }
expect(response.status).to eq(400)
end
it 'can clear the profile background' do
delete "/u/#{user.username}/preferences/user_image.json", params: { type: 'profile_background' }
expect(user.reload.profile_background_upload).to eq(nil)
expect(response.status).to eq(200)
end
end
end
describe '#destroy' do
it 'raises an error when not logged in' do
delete "/u/nobody.json"
expect(response.status).to eq(403)
end
context 'while logged in' do
fab!(:user) { Fabricate(:user) }
fab!(:another_user) { Fabricate(:user) }
before do
sign_in(user)
end
it 'raises an error when you cannot delete your account' do
UserDestroyer.any_instance.expects(:destroy).never
stat = user.user_stat
stat.post_count = 3
stat.save!
delete "/u/#{user.username}.json"
expect(response).to be_forbidden
end
it "raises an error when you try to delete someone else's account" do
UserDestroyer.any_instance.expects(:destroy).never
delete "/u/#{another_user.username}.json"
expect(response).to be_forbidden
end
it "deletes your account when you're allowed to" do
UserDestroyer.any_instance.expects(:destroy).with(user, anything).returns(user)
delete "/u/#{user.username}.json"
expect(response.status).to eq(200)
end
end
end
describe '#ignore' do
it 'raises an error when not logged in' do
put "/u/#{user.username}/notification_level.json", params: { notification_level: "" }
expect(response.status).to eq(403)
end
context 'while logged in' do
fab!(:user) { Fabricate(:user, trust_level: 2) }
fab!(:another_user) { Fabricate(:user) }
before do
sign_in(user)
end
let!(:ignored_user) { Fabricate(:ignored_user, user: user, ignored_user: another_user) }
let!(:muted_user) { Fabricate(:muted_user, user: user, muted_user: another_user) }
context 'when changing notification level to normal' do
it 'changes notification level to normal' do
put "/u/#{another_user.username}/notification_level.json", params: { notification_level: "normal" }
expect(IgnoredUser.count).to eq(0)
expect(MutedUser.count).to eq(0)
end
end
context 'when changing notification level to mute' do
it 'changes notification level to mute' do
put "/u/#{another_user.username}/notification_level.json", params: { notification_level: "mute" }
expect(IgnoredUser.count).to eq(0)
expect(MutedUser.find_by(user_id: user.id, muted_user_id: another_user.id)).to be_present
end
end
context 'when changing notification level to ignore' do
it 'changes notification level to ignore' do
put "/u/#{another_user.username}/notification_level.json", params: { notification_level: "ignore" }
expect(MutedUser.count).to eq(0)
expect(IgnoredUser.find_by(user_id: user.id, ignored_user_id: another_user.id)).to be_present
end
context 'when expiring_at param is set' do
it 'changes notification level to ignore' do
freeze_time(Time.now) do
expiring_at = 3.days.from_now
put "/u/#{another_user.username}/notification_level.json", params: { notification_level: "ignore", expiring_at: expiring_at }
ignored_user = IgnoredUser.find_by(user_id: user.id, ignored_user_id: another_user.id)
expect(ignored_user).to be_present
expect(ignored_user.expiring_at.to_i).to eq(expiring_at.to_i)
expect(MutedUser.count).to eq(0)
end
end
end
end
end
end
describe "for user with period in username" do
fab!(:user_with_period) { Fabricate(:user, username: "myname.test") }
it "still works" do
sign_in(user_with_period)
UserDestroyer.any_instance.expects(:destroy).with(user_with_period, anything).returns(user_with_period)
delete "/u/#{user_with_period.username}", xhr: true
expect(response.status).to eq(200)
end
end
describe '#my_redirect' do
it "redirects if the user is not logged in" do
get "/my/wat.json"
expect(response).to be_redirect
end
context "when the user is logged in" do
let!(:user) { sign_in(Fabricate(:user)) }
it "will not redirect to an invalid path" do
get "/my/wat/..password.txt"
expect(response).not_to be_redirect
end
it "will redirect to an valid path" do
get "/my/preferences.json"
expect(response).to be_redirect
end
it "permits forward slashes" do
get "/my/activity/posts.json"
expect(response).to be_redirect
end
end
end
describe '#check_emails' do
it 'raises an error when not logged in' do
get "/u/zogstrip/emails.json"
expect(response.status).to eq(403)
end
context 'while logged in' do
let(:sign_in_admin) { sign_in(Fabricate(:admin)) }
it "raises an error when you aren't allowed to check emails" do
sign_in(Fabricate(:user))
get "/u/#{Fabricate(:user).username}/emails.json"
expect(response).to be_forbidden
end
it "returns emails and associated_accounts for self" do
user = Fabricate(:user)
sign_in(user)
get "/u/#{user.username}/emails.json"
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["email"]).to eq(user.email)
expect(json["secondary_emails"]).to eq(user.secondary_emails)
expect(json["associated_accounts"]).to eq([])
end
it "returns emails and associated_accounts when you're allowed to see them" do
user = Fabricate(:user)
sign_in_admin
get "/u/#{user.username}/emails.json"
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["email"]).to eq(user.email)
expect(json["secondary_emails"]).to eq(user.secondary_emails)
expect(json["associated_accounts"]).to eq([])
end
it "works on inactive users" do
inactive_user = Fabricate(:user, active: false)
sign_in_admin
get "/u/#{inactive_user.username}/emails.json"
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["email"]).to eq(inactive_user.email)
expect(json["secondary_emails"]).to eq(inactive_user.secondary_emails)
expect(json["associated_accounts"]).to eq([])
end
end
end
describe '#is_local_username' do
fab!(:user) { Fabricate(:user) }
fab!(:group) { Fabricate(:group, name: "Discourse", mentionable_level: Group::ALIAS_LEVELS[:everyone]) }
let(:unmentionable) {
Fabricate(:group, name: "Unmentionable", mentionable_level: Group::ALIAS_LEVELS[:nobody])
}
fab!(:topic) { Fabricate(:topic) }
fab!(:allowed_user) { Fabricate(:user) }
let(:private_topic) { Fabricate(:private_message_topic, user: allowed_user) }
it "finds the user" do
get "/u/is_local_username.json", params: { username: user.username }
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["valid"][0]).to eq(user.username)
end
it "finds the group" do
sign_in(user)
get "/u/is_local_username.json", params: { username: group.name }
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["valid_groups"]).to include(group.name)
expect(json["mentionable_groups"].find { |g| g['name'] == group.name }).to be_present
end
it "finds unmentionable groups" do
sign_in(user)
get "/u/is_local_username.json", params: { username: unmentionable.name }
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["valid_groups"]).to include(unmentionable.name)
expect(json["mentionable_groups"]).to be_blank
end
it "supports multiples usernames" do
get "/u/is_local_username.json", params: { usernames: [user.username, "system"] }
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["valid"].size).to eq(2)
end
it "never includes staged accounts" do
staged = Fabricate(:user, staged: true)
get "/u/is_local_username.json", params: { usernames: [staged.username] }
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["valid"].size).to eq(0)
end
it "returns user who cannot see topic" do
Guardian.any_instance.expects(:can_see?).with(topic).returns(false)
get "/u/is_local_username.json", params: {
usernames: [user.username], topic_id: topic.id
}
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["cannot_see"].size).to eq(1)
end
it "never returns a user who can see the topic" do
Guardian.any_instance.expects(:can_see?).with(topic).returns(true)
get "/u/is_local_username.json", params: {
usernames: [user.username], topic_id: topic.id
}
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["cannot_see"].size).to eq(0)
end
it "returns user who cannot see a private topic" do
Guardian.any_instance.expects(:can_see?).with(private_topic).returns(false)
get "/u/is_local_username.json", params: {
usernames: [user.username], topic_id: private_topic.id
}
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["cannot_see"].size).to eq(1)
end
it "never returns a user who can see the topic" do
Guardian.any_instance.expects(:can_see?).with(private_topic).returns(true)
get "/u/is_local_username.json", params: {
usernames: [allowed_user.username], topic_id: private_topic.id
}
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["cannot_see"].size).to eq(0)
end
end
describe '#topic_tracking_state' do
fab!(:user) { Fabricate(:user) }
context 'anon' do
it "raises an error on anon for topic_tracking_state" do
get "/u/#{user.username}/topic-tracking-state.json"
expect(response.status).to eq(403)
end
end
context 'logged on' do
it "detects new topic" do
sign_in(user)
topic = Fabricate(:topic)
get "/u/#{user.username}/topic-tracking-state.json"
expect(response.status).to eq(200)
states = JSON.parse(response.body)
expect(states[0]["topic_id"]).to eq(topic.id)
end
end
end
describe '#summary' do
it "generates summary info" do
user = Fabricate(:user)
create_post(user: user)
get "/u/#{user.username_lower}/summary.json"
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["user_summary"]["topic_count"]).to eq(1)
expect(json["user_summary"]["post_count"]).to eq(0)
end
it "returns 404 for a hidden profile" do
user = Fabricate(:user)
user.user_option.update_column(:hide_profile_and_presence, true)
get "/u/#{user.username_lower}/summary.json"
expect(response.status).to eq(404)
end
end
describe '#confirm_admin' do
it "fails without a valid token" do
get "/u/confirm-admin/invalid-token.json"
expect(response).not_to be_successful
end
it "fails with a missing token" do
get "/u/confirm-admin/a0a0a0a0a0.json"
expect(response).to_not be_successful
end
it "succeeds with a valid code as anonymous" do
user = Fabricate(:user)
ac = AdminConfirmation.new(user, Fabricate(:admin))
ac.create_confirmation
get "/u/confirm-admin/#{ac.token}.json"
expect(response.status).to eq(200)
user.reload
expect(user.admin?).to eq(false)
end
it "succeeds with a valid code when logged in as that user" do
admin = sign_in(Fabricate(:admin))
user = Fabricate(:user)
ac = AdminConfirmation.new(user, admin)
ac.create_confirmation
get "/u/confirm-admin/#{ac.token}.json", params: { token: ac.token }
expect(response.status).to eq(200)
user.reload
expect(user.admin?).to eq(false)
end
it "fails if you're logged in as a different account" do
sign_in(Fabricate(:admin))
user = Fabricate(:user)
ac = AdminConfirmation.new(user, Fabricate(:admin))
ac.create_confirmation
get "/u/confirm-admin/#{ac.token}.json"
expect(response).to_not be_successful
user.reload
expect(user.admin?).to eq(false)
end
describe "post" do
it "gives the user admin access when POSTed" do
user = Fabricate(:user)
ac = AdminConfirmation.new(user, Fabricate(:admin))
ac.create_confirmation
post "/u/confirm-admin/#{ac.token}.json"
expect(response.status).to eq(200)
user.reload
expect(user.admin?).to eq(true)
end
end
end
describe '#update_activation_email' do
before do
UsersController.any_instance.stubs(:honeypot_value).returns(nil)
UsersController.any_instance.stubs(:challenge_value).returns(nil)
end
let(:post_user) do
post "/u.json", params: {
username: "osamatest",
password: "strongpassword",
email: "osama@example.com"
}
user = User.where(username: "osamatest").first
user.active = false
user.save!
user
end
context "with a session variable" do
it "raises an error with an invalid session value" do
post_user
post "/u.json", params: {
username: "osamatest2",
password: "strongpassword2",
email: "osama22@example.com"
}
user = User.where(username: "osamatest2").first
user.destroy
put "/u/update-activation-email.json", params: {
email: 'osamaupdated@example.com'
}
expect(response.status).to eq(403)
end
it "raises an error for an active user" do
user = post_user
user.update(active: true)
user.save!
put "/u/update-activation-email.json", params: {
email: 'osama@example.com'
}
expect(response.status).to eq(403)
end
it "raises an error when logged in" do
sign_in(Fabricate(:moderator))
post_user
put "/u/update-activation-email.json", params: {
email: 'updatedemail@example.com'
}
expect(response.status).to eq(403)
end
it "raises an error when the new email is taken" do
active_user = Fabricate(:user)
post_user
put "/u/update-activation-email.json", params: {
email: active_user.email
}
expect(response.status).to eq(422)
end
it "raises an error when the email is blacklisted" do
post_user
SiteSetting.email_domains_blacklist = 'example.com'
put "/u/update-activation-email.json", params: { email: 'test@example.com' }
expect(response.status).to eq(422)
end
it "can be updated" do
user = post_user
token = user.email_tokens.first
put "/u/update-activation-email.json", params: {
email: 'updatedemail@example.com'
}
expect(response.status).to eq(200)
user.reload
expect(user.email).to eq('updatedemail@example.com')
expect(user.email_tokens.where(email: 'updatedemail@example.com', expired: false)).to be_present
token.reload
expect(token.expired?).to eq(true)
end
end
context "with a username and password" do
it "raises an error with an invalid username" do
put "/u/update-activation-email.json", params: {
username: 'eviltrout',
password: 'invalid-password',
email: 'updatedemail@example.com'
}
expect(response.status).to eq(403)
end
it "raises an error with an invalid password" do
put "/u/update-activation-email.json", params: {
username: Fabricate(:inactive_user).username,
password: 'invalid-password',
email: 'updatedemail@example.com'
}
expect(response.status).to eq(403)
end
it "raises an error for an active user" do
put "/u/update-activation-email.json", params: {
username: Fabricate(:walter_white).username,
password: 'letscook',
email: 'updatedemail@example.com'
}
expect(response.status).to eq(403)
end
it "raises an error when logged in" do
sign_in(Fabricate(:moderator))
put "/u/update-activation-email.json", params: {
username: Fabricate(:inactive_user).username,
password: 'qwerqwer123',
email: 'updatedemail@example.com'
}
expect(response.status).to eq(403)
end
it "raises an error when the new email is taken" do
user = Fabricate(:user)
put "/u/update-activation-email.json", params: {
username: Fabricate(:inactive_user).username,
password: 'qwerqwer123',
email: user.email
}
expect(response.status).to eq(422)
end
it "can be updated" do
user = Fabricate(:inactive_user)
token = user.email_tokens.first
put "/u/update-activation-email.json", params: {
username: user.username,
password: 'qwerqwer123',
email: 'updatedemail@example.com'
}
expect(response.status).to eq(200)
user.reload
expect(user.email).to eq('updatedemail@example.com')
expect(user.email_tokens.where(email: 'updatedemail@example.com', expired: false)).to be_present
token.reload
expect(token.expired?).to eq(true)
end
end
end
describe '#show' do
context "anon" do
let(:user) { Discourse.system_user }
it "returns success" do
get "/u/#{user.username}.json"
expect(response.status).to eq(200)
parsed = JSON.parse(response.body)["user"]
expect(parsed['username']).to eq(user.username)
expect(parsed["profile_hidden"]).to be_blank
expect(parsed["trust_level"]).to be_present
end
it "returns a hidden profile" do
user.user_option.update_column(:hide_profile_and_presence, true)
get "/u/#{user.username}.json"
expect(response.status).to eq(200)
parsed = JSON.parse(response.body)["user"]
expect(parsed["username"]).to eq(user.username)
expect(parsed["profile_hidden"]).to eq(true)
expect(parsed["trust_level"]).to be_blank
end
it "should redirect to login page for anonymous user when profiles are hidden" do
SiteSetting.hide_user_profiles_from_public = true
get "/u/#{user.username}.json"
expect(response).to redirect_to '/login'
end
describe "user profile views" do
fab!(:other_user) { Fabricate(:user) }
it "should track a user profile view for an anon user" do
get "/"
UserProfileView.expects(:add).with(other_user.user_profile.id, request.remote_ip, nil)
get "/u/#{other_user.username}.json"
end
it "skips tracking" do
UserProfileView.expects(:add).never
get "/u/#{user.username}.json", params: { skip_track_visit: true }
end
end
end
context "logged in" do
before do
sign_in(user)
end
fab!(:user) { Fabricate(:user) }
it 'returns success' do
get "/u/#{user.username}.json"
expect(response.status).to eq(200)
expect(response.headers['X-Robots-Tag']).to eq('noindex')
json = JSON.parse(response.body)
expect(json["user"]["has_title_badges"]).to eq(false)
end
it "returns not found when the username doesn't exist" do
get "/u/madeuppity.json"
expect(response).not_to be_successful
end
it 'returns not found when the user is inactive' do
inactive = Fabricate(:user, active: false)
get "/u/#{inactive.username}.json"
expect(response).not_to be_successful
end
it 'returns success when show_inactive_accounts is true and user is logged in' do
SiteSetting.show_inactive_accounts = true
inactive = Fabricate(:user, active: false)
get "/u/#{inactive.username}.json"
expect(response.status).to eq(200)
end
it "raises an error on invalid access" do
Guardian.any_instance.expects(:can_see?).with(user).returns(false)
get "/u/#{user.username}.json"
expect(response).to be_forbidden
end
describe "user profile views" do
fab!(:other_user) { Fabricate(:user) }
it "should track a user profile view for a signed in user" do
UserProfileView.expects(:add).with(other_user.user_profile.id, request.remote_ip, user.id)
get "/u/#{other_user.username}.json"
end
it "should not track a user profile view for a user viewing his own profile" do
UserProfileView.expects(:add).never
get "/u/#{user.username}.json"
end
it "skips tracking" do
UserProfileView.expects(:add).never
get "/u/#{user.username}.json", params: { skip_track_visit: true }
end
end
context "fetching a user by external_id" do
before { user.create_single_sign_on_record(external_id: '997', last_payload: '') }
it "returns fetch for a matching external_id" do
get "/u/by-external/997.json"
expect(response.status).to eq(200)
expect(JSON.parse(response.body)["user"]["username"]).to eq(user.username)
end
it "returns not found when external_id doesn't match" do
get "/u/by-external/99.json"
expect(response).not_to be_successful
end
end
describe "include_post_count_for" do
fab!(:admin) { Fabricate(:admin) }
fab!(:topic) { Fabricate(:topic) }
before do
Fabricate(:post, user: user, topic: topic)
Fabricate(:post, user: admin, topic: topic)
Fabricate(:post, user: admin, topic: topic, post_type: Post.types[:whisper])
end
it "includes only visible posts" do
get "/u/#{admin.username}.json", params: { include_post_count_for: topic.id }
topic_post_count = JSON.parse(response.body).dig("user", "topic_post_count")
expect(topic_post_count[topic.id.to_s]).to eq(1)
end
it "includes all post types for staff members" do
sign_in(admin)
get "/u/#{admin.username}.json", params: { include_post_count_for: topic.id }
topic_post_count = JSON.parse(response.body).dig("user", "topic_post_count")
expect(topic_post_count[topic.id.to_s]).to eq(2)
end
end
end
it "should be able to view a user" do
get "/u/#{user.username}"
expect(response.status).to eq(200)
expect(response.body).to include(user.username)
end
describe 'when username contains a period' do
before do
user.update!(username: 'test.test')
end
it "should be able to view a user" do
get "/u/#{user.username}"
expect(response.status).to eq(200)
expect(response.body).to include(user.username)
end
end
end
describe "#show_card" do
context "anon" do
let(:user) { Discourse.system_user }
it "returns success" do
get "/u/#{user.username}/card.json"
expect(response.status).to eq(200)
parsed = JSON.parse(response.body)["user"]
expect(parsed["username"]).to eq(user.username)
expect(parsed["profile_hidden"]).to be_blank
expect(parsed["trust_level"]).to be_present
end
it "should redirect to login page for anonymous user when profiles are hidden" do
SiteSetting.hide_user_profiles_from_public = true
get "/u/#{user.username}/card.json"
expect(response).to redirect_to '/login'
end
end
context "logged in" do
before do
sign_in(user)
end
fab!(:user) { Fabricate(:user) }
it 'works correctly' do
get "/u/#{user.username}/card.json"
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["user"]["associated_accounts"]).to eq(nil) # Not serialized in card
expect(json["user"]["username"]).to eq(user.username)
end
it "returns not found when the username doesn't exist" do
get "/u/madeuppity/card.json"
expect(response).not_to be_successful
end
it "raises an error on invalid access" do
Guardian.any_instance.expects(:can_see?).with(user).returns(false)
get "/u/#{user.username}/card.json"
expect(response).to be_forbidden
end
end
end
describe '#badges' do
it "renders fine by default" do
get "/u/#{user.username}/badges"
expect(response.status).to eq(200)
end
it "fails if badges are disabled" do
SiteSetting.enable_badges = false
get "/u/#{user.username}/badges"
expect(response.status).to eq(404)
end
end
describe "#account_created" do
it "returns a message when no session is present" do
get "/u/account-created"
expect(response.status).to eq(200)
body = response.body
expect(body).to match(I18n.t('activation.missing_session'))
end
it "redirects when the user is logged in" do
sign_in(Fabricate(:user))
get "/u/account-created"
expect(response).to redirect_to("/")
end
context 'when cookies contains a destination URL' do
it 'should redirect to the URL' do
sign_in(Fabricate(:user))
destination_url = 'http://thisisasite.com/somepath'
cookies[:destination_url] = destination_url
get "/u/account-created"
expect(response).to redirect_to(destination_url)
end
end
context "when the user account is created" do
include ApplicationHelper
it "returns the message when set in the session" do
user = create_user
get "/u/account-created"
expect(response.status).to eq(200)
expect(response.body).to have_tag("div#data-preloaded") do |element|
json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
expect(json['accountCreated']).to include(
"{\"message\":\"#{I18n.t("login.activate_email", email: user.email).gsub!("</", "<\\/")}\",\"show_controls\":true,\"username\":\"#{user.username}\",\"email\":\"#{user.email}\"}"
)
end
end
end
end
describe '#search_users' do
fab!(:topic) { Fabricate :topic }
let(:user) { Fabricate :user, username: "joecabot", name: "Lawrence Tierney" }
let(:post1) { Fabricate(:post, user: user, topic: topic) }
before do
SearchIndexer.enable
post1
end
it "searches when provided the term only" do
get "/u/search/users.json", params: { term: user.name.split(" ").last }
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["users"].map { |u| u["username"] }).to include(user.username)
end
it "searches when provided the topic only" do
get "/u/search/users.json", params: { topic_id: topic.id }
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["users"].map { |u| u["username"] }).to include(user.username)
end
it "searches when provided the term and topic" do
get "/u/search/users.json", params: {
term: user.name.split(" ").last, topic_id: topic.id
}
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["users"].map { |u| u["username"] }).to include(user.username)
end
it "searches only for users who have access to private topic" do
searching_user = Fabricate(:user)
privileged_user = Fabricate(:user, trust_level: 4, username: "joecabit", name: "Lawrence Tierney")
privileged_group = Fabricate(:group)
privileged_group.add(searching_user)
privileged_group.add(privileged_user)
privileged_group.save
category = Fabricate(:category)
category.set_permissions(privileged_group => :readonly)
category.save
private_topic = Fabricate(:topic, category: category)
sign_in(searching_user)
get "/u/search/users.json", params: {
term: user.name.split(" ").last, topic_id: private_topic.id, topic_allowed_users: "true"
}
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json["users"].map { |u| u["username"] }).to_not include(user.username)
expect(json["users"].map { |u| u["username"] }).to include(privileged_user.username)
end
it "interprets blank category id correctly" do
pm_topic = Fabricate(:private_message_post).topic
sign_in(pm_topic.user)
get "/u/search/users.json", params: {
term: "", topic_id: pm_topic.id, category_id: ""
}
expect(response.status).to eq(200)
end
context "when `enable_names` is true" do
before do
SiteSetting.enable_names = true
end
it "returns names" do
get "/u/search/users.json", params: { term: user.name }
json = JSON.parse(response.body)
expect(json["users"].map { |u| u["name"] }).to include(user.name)
end
end
context "when `enable_names` is false" do
before do
SiteSetting.enable_names = false
end
it "returns names" do
get "/u/search/users.json", params: { term: user.name }
json = JSON.parse(response.body)
expect(json["users"].map { |u| u["name"] }).not_to include(user.name)
end
end
context 'groups' do
let!(:mentionable_group) do
Fabricate(:group,
mentionable_level: Group::ALIAS_LEVELS[:everyone],
messageable_level: Group::ALIAS_LEVELS[:nobody],
visibility_level: Group.visibility_levels[:public],
name: 'aaa1'
)
end
let!(:mentionable_group_2) do
Fabricate(:group,
mentionable_level: Group::ALIAS_LEVELS[:everyone],
messageable_level: Group::ALIAS_LEVELS[:nobody],
visibility_level: Group.visibility_levels[:logged_on_users],
name: 'aaa2'
)
end
let!(:messageable_group) do
Fabricate(:group,
mentionable_level: Group::ALIAS_LEVELS[:nobody],
messageable_level: Group::ALIAS_LEVELS[:everyone],
visibility_level: Group.visibility_levels[:logged_on_users],
name: 'aaa3'
)
end
let!(:private_group) do
Fabricate(:group,
mentionable_level: Group::ALIAS_LEVELS[:members_mods_and_admins],
messageable_level: Group::ALIAS_LEVELS[:members_mods_and_admins],
visibility_level: Group.visibility_levels[:members],
name: 'aaa4'
)
end
describe 'when signed in' do
before do
sign_in(user)
end
it "does not search for groups if there is no term" do
get "/u/search/users.json", params: { include_groups: "true" }
expect(response.status).to eq(200)
groups = JSON.parse(response.body)["groups"]
expect(groups).to eq(nil)
end
it "only returns visible groups" do
get "/u/search/users.json", params: { include_groups: "true", term: 'a' }
expect(response.status).to eq(200)
groups = JSON.parse(response.body)["groups"]
expect(groups.map { |group| group['name'] })
.to_not include(private_group.name)
end
it "doesn't search for groups" do
get "/u/search/users.json", params: {
include_mentionable_groups: 'false',
include_messageable_groups: 'false',
term: 'a'
}
expect(response.status).to eq(200)
expect(JSON.parse(response.body)).not_to have_key(:groups)
end
it "searches for messageable groups" do
get "/u/search/users.json", params: {
include_mentionable_groups: 'false',
include_messageable_groups: 'true',
term: 'a'
}
expect(response.status).to eq(200)
expect(JSON.parse(response.body)["groups"].map { |group| group['name'] })
.to contain_exactly(messageable_group.name, Group.find(Group::AUTO_GROUPS[:moderators]).name)
end
it 'searches for mentionable groups' do
get "/u/search/users.json", params: {
include_messageable_groups: 'false',
include_mentionable_groups: 'true',
term: 'a'
}
expect(response.status).to eq(200)
groups = JSON.parse(response.body)["groups"]
expect(groups.map { |group| group['name'] })
.to contain_exactly(mentionable_group.name, mentionable_group_2.name)
end
end
describe 'when not signed in' do
it 'should not include mentionable/messageable groups' do
get "/u/search/users.json", params: {
include_mentionable_groups: 'false',
include_messageable_groups: 'false',
term: 'a'
}
expect(response.status).to eq(200)
expect(JSON.parse(response.body)).not_to have_key(:groups)
get "/u/search/users.json", params: {
include_mentionable_groups: 'false',
include_messageable_groups: 'true',
term: 'a'
}
expect(response.status).to eq(200)
expect(JSON.parse(response.body)).not_to have_key(:groups)
get "/u/search/users.json", params: {
include_messageable_groups: 'false',
include_mentionable_groups: 'true',
term: 'a'
}
expect(response.status).to eq(200)
expect(JSON.parse(response.body)).not_to have_key(:groups)
end
end
describe 'when searching by group name' do
fab!(:exclusive_group) { Fabricate(:group) }
it 'return results if the user is a group member' do
exclusive_group.add(user)
get "/u/search/users.json", params: {
group: exclusive_group.name,
term: user.username
}
expect(users_found).to contain_exactly(user.username)
end
it 'does not return results if the user is not a group member' do
get "/u/search/users.json", params: {
group: exclusive_group.name,
term: user.username
}
expect(users_found).to be_empty
end
it 'returns results if the user is member of one of the groups' do
exclusive_group.add(user)
get "/u/search/users.json", params: {
groups: [exclusive_group.name],
term: user.username
}
expect(users_found).to contain_exactly(user.username)
end
it 'does not return results if the user is not a member of the groups' do
get "/u/search/users.json", params: {
groups: [exclusive_group.name],
term: user.username
}
expect(users_found).to be_empty
end
def users_found
JSON.parse(response.body)['users'].map { |u| u['username'] }
end
end
end
end
describe '#user_preferences_redirect' do
it 'requires the user to be logged in' do
get '/user_preferences'
expect(response.status).to eq(404)
end
it "redirects to their profile when logged in" do
sign_in(user)
get '/user_preferences'
expect(response).to redirect_to("/u/#{user.username_lower}/preferences")
end
end
describe '#email_login' do
before do
SiteSetting.enable_local_logins_via_email = true
end
it "enqueues the right email" do
post "/u/email-login.json", params: { login: user.email }
expect(response.status).to eq(200)
expect(JSON.parse(response.body)['user_found']).to eq(true)
job_args = Jobs::CriticalUserEmail.jobs.last["args"].first
expect(job_args["user_id"]).to eq(user.id)
expect(job_args["type"]).to eq("email_login")
expect(job_args["email_token"]).to eq(user.email_tokens.last.token)
end
describe 'when enable_local_logins_via_email is disabled' do
before do
SiteSetting.enable_local_logins_via_email = false
end
it 'should return the right response' do
post "/u/email-login.json", params: { login: user.email }
expect(response.status).to eq(404)
end
end
describe 'when username or email is not valid' do
it 'should not enqueue the email to login' do
post "/u/email-login.json", params: { login: '@random' }
expect(response.status).to eq(200)
expect(JSON.parse(response.body)['user_found']).to eq(false)
expect(Jobs::CriticalUserEmail.jobs).to eq([])
end
end
describe 'when hide_email_address_taken is true' do
it 'should return the right response' do
SiteSetting.hide_email_address_taken = true
post "/u/email-login.json", params: { login: user.email }
expect(response.status).to eq(200)
expect(JSON.parse(response.body).has_key?('user_found')).to eq(false)
end
end
describe "when user is already logged in" do
it 'should redirect to the root path' do
sign_in(user)
post "/u/email-login.json", params: { login: user.email }
expect(response).to redirect_to("/")
end
end
end
describe '#create_second_factor_totp' do
context 'when not logged in' do
it 'should return the right response' do
post "/users/second_factors.json", params: {
password: 'wrongpassword'
}
expect(response.status).to eq(403)
end
end
context 'when logged in' do
before do
sign_in(user)
end
describe 'create 2fa request' do
it 'fails on incorrect password' do
ApplicationController.any_instance.expects(:secure_session).returns("confirmed-password-#{user.id}" => "false")
post "/users/create_second_factor_totp.json"
expect(response.status).to eq(403)
end
describe 'when local logins are disabled' do
it 'should return the right response' do
SiteSetting.enable_local_logins = false
post "/users/create_second_factor_totp.json"
expect(response.status).to eq(404)
end
end
describe 'when SSO is enabled' do
it 'should return the right response' do
SiteSetting.sso_url = 'http://someurl.com'
SiteSetting.enable_sso = true
post "/users/create_second_factor_totp.json"
expect(response.status).to eq(404)
end
end
it 'succeeds on correct password' do
ApplicationController.any_instance.stubs(:secure_session).returns("confirmed-password-#{user.id}" => "true")
post "/users/create_second_factor_totp.json"
expect(response.status).to eq(200)
response_body = JSON.parse(response.body)
expect(response_body['key']).to be_present
expect(response_body['qr']).to be_present
end
end
end
end
describe "#enable_second_factor_totp" do
before do
sign_in(user)
end
def create_totp
stub_secure_session_confirmed
post "/users/create_second_factor_totp.json"
end
it "creates a totp for the user successfully" do
create_totp
staged_totp_key = read_secure_session["staged-totp-#{user.id}"]
token = ROTP::TOTP.new(staged_totp_key).now
post "/users/enable_second_factor_totp.json", params: { name: "test", second_factor_token: token }
expect(response.status).to eq(200)
expect(user.user_second_factors.count).to eq(1)
end
context "when an incorrect token is provided" do
before do
create_totp
post "/users/enable_second_factor_totp.json", params: { name: "test", second_factor_token: "123456" }
end
it "shows a helpful error message to the user" do
expect(JSON.parse(response.body)['error']).to eq(I18n.t("login.invalid_second_factor_code"))
end
end
context "when a name is not provided" do
before do
create_totp
post "/users/enable_second_factor_totp.json", params: { second_factor_token: "123456" }
end
it "shows a helpful error message to the user" do
expect(JSON.parse(response.body)['error']).to eq(I18n.t("login.missing_second_factor_name"))
end
end
context "when a token is not provided" do
before do
create_totp
post "/users/enable_second_factor_totp.json", params: { name: "test" }
end
it "shows a helpful error message to the user" do
expect(JSON.parse(response.body)['error']).to eq(I18n.t("login.missing_second_factor_code"))
end
end
end
describe '#update_second_factor' do
let(:user_second_factor) { Fabricate(:user_second_factor_totp, user: user) }
context 'when not logged in' do
it 'should return the right response' do
put "/users/second_factor.json"
expect(response.status).to eq(403)
end
end
context 'when logged in' do
before do
sign_in(user)
user_second_factor
end
context 'when user has totp setup' do
context 'when token is missing' do
it 'returns the right response' do
put "/users/second_factor.json", params: {
disable: 'true',
second_factor_target: UserSecondFactor.methods[:totp],
id: user_second_factor.id
}
expect(response.status).to eq(403)
end
end
context 'when token is valid' do
before do
stub_secure_session_confirmed
end
it 'should allow second factor for the user to be renamed' do
put "/users/second_factor.json", params: {
name: 'renamed',
second_factor_target: UserSecondFactor.methods[:totp],
id: user_second_factor.id
}
expect(response.status).to eq(200)
expect(user.reload.user_second_factors.totps.first.name).to eq("renamed")
end
it 'should allow second factor for the user to be disabled' do
put "/users/second_factor.json", params: {
disable: 'true',
second_factor_target: UserSecondFactor.methods[:totp],
id: user_second_factor.id
}
expect(response.status).to eq(200)
expect(user.reload.user_second_factors.totps.first).to eq(nil)
end
end
end
context "when user is updating backup codes" do
context 'when token is missing' do
it 'returns the right response' do
put "/users/second_factor.json", params: {
second_factor_target: UserSecondFactor.methods[:backup_codes]
}
expect(response.status).to eq(403)
end
end
context 'when token is valid' do
before do
ApplicationController.any_instance.stubs(:secure_session).returns("confirmed-password-#{user.id}" => "true")
end
it 'should allow second factor backup for the user to be disabled' do
put "/users/second_factor.json", params: {
second_factor_target: UserSecondFactor.methods[:backup_codes],
disable: 'true'
}
expect(response.status).to eq(200)
expect(user.reload.user_second_factors.backup_codes).to be_empty
end
end
end
end
end
describe '#create_second_factor_backup' do
let(:user_second_factor) { Fabricate(:user_second_factor_totp, user: user) }
context 'when not logged in' do
it 'should return the right response' do
put "/users/second_factors_backup.json", params: {
second_factor_token: 'wrongtoken',
second_factor_method: UserSecondFactor.methods[:totp]
}
expect(response.status).to eq(403)
end
end
context 'when logged in' do
before do
sign_in(user)
end
describe 'create 2fa request' do
it 'fails on incorrect password' do
ApplicationController.any_instance.expects(:secure_session).returns("confirmed-password-#{user.id}" => "false")
put "/users/second_factors_backup.json"
expect(response.status).to eq(403)
end
describe 'when local logins are disabled' do
it 'should return the right response' do
SiteSetting.enable_local_logins = false
put "/users/second_factors_backup.json"
expect(response.status).to eq(404)
end
end
describe 'when SSO is enabled' do
it 'should return the right response' do
SiteSetting.sso_url = 'http://someurl.com'
SiteSetting.enable_sso = true
put "/users/second_factors_backup.json"
expect(response.status).to eq(404)
end
end
it 'succeeds on correct password' do
ApplicationController.any_instance.expects(:secure_session).returns("confirmed-password-#{user.id}" => "true")
put "/users/second_factors_backup.json"
expect(response.status).to eq(200)
response_body = JSON.parse(response.body)
# we use SecureRandom.hex(16) for backup codes, ensure this continues to be the case
expect(response_body['backup_codes'].map(&:length)).to eq([32] * 10)
end
end
end
end
describe "#create_second_factor_security_key" do
it "stores the challenge in the session and returns challenge data, user id, and supported algorithms" do
create_second_factor_security_key
secure_session = read_secure_session
response_parsed = JSON.parse(response.body)
expect(response_parsed["challenge"]).to eq(
Webauthn.challenge(user, secure_session)
)
expect(response_parsed["rp_id"]).to eq(
Webauthn.rp_id(user, secure_session)
)
expect(response_parsed["rp_name"]).to eq(
Webauthn.rp_name(user, secure_session)
)
expect(response_parsed["user_secure_id"]).to eq(
user.reload.create_or_fetch_secure_identifier
)
expect(response_parsed["supported_algoriths"]).to eq(
::Webauthn::SUPPORTED_ALGORITHMS
)
end
context "if the user has security key credentials already" do
let!(:user_security_key) { Fabricate(:user_security_key_with_random_credential, user: user) }
it "returns those existing active credentials" do
create_second_factor_security_key
response_parsed = JSON.parse(response.body)
expect(response_parsed["existing_active_credential_ids"]).to eq(
[user_security_key.credential_id]
)
end
end
end
describe "#register_second_factor_security_key" do
context "when creation parameters are valid" do
it "creates a security key for the user" do
simulate_localhost_webauthn_challenge
create_second_factor_security_key
response_parsed = JSON.parse(response.body)
post "/u/register_second_factor_security_key.json", params: valid_security_key_create_post_data
expect(user.security_keys.count).to eq(1)
expect(user.security_keys.last.credential_id).to eq(valid_security_key_create_post_data[:rawId])
expect(user.security_keys.last.name).to eq(valid_security_key_create_post_data[:name])
end
end
context "when the creation parameters are invalid" do
it "shows a security key error and does not create a key" do
stub_as_dev_localhost
create_second_factor_security_key
response_parsed = JSON.parse(response.body)
post "/u/register_second_factor_security_key.json", params: {
id: "bad id",
rawId: "bad rawId",
type: "public-key",
attestation: "bad attestation",
clientData: Base64.encode64('{"bad": "json"}'),
name: "My Bad Key"
}
expect(user.security_keys.count).to eq(0)
expect(JSON.parse(response.body)["error"]).to eq(I18n.t("webauthn.validation.invalid_type_error"))
end
end
end
describe '#revoke_account' do
fab!(:other_user) { Fabricate(:user) }
it 'errors for unauthorised users' do
post "/u/#{user.username}/preferences/revoke-account.json", params: {
provider_name: 'facebook'
}
expect(response.status).to eq(403)
sign_in(other_user)
post "/u/#{user.username}/preferences/revoke-account.json", params: {
provider_name: 'facebook'
}
expect(response.status).to eq(403)
end
context 'while logged in' do
before do
sign_in(user)
end
it 'returns an error when there is no matching account' do
post "/u/#{user.username}/preferences/revoke-account.json", params: {
provider_name: 'facebook'
}
expect(response.status).to eq(404)
end
context "with fake provider" do
let(:authenticator) do
Class.new(Auth::Authenticator) do
attr_accessor :can_revoke
def name
"testprovider"
end
def enabled?
true
end
def description_for_user(user)
"an account"
end
def can_revoke?
can_revoke
end
def revoke(user, skip_remote: false)
true
end
end.new
end
before do
DiscoursePluginRegistry.register_auth_provider(Auth::AuthProvider.new(authenticator: authenticator))
end
after do
DiscoursePluginRegistry.reset!
end
it 'returns an error when revoking is not allowed' do
authenticator.can_revoke = false
post "/u/#{user.username}/preferences/revoke-account.json", params: {
provider_name: 'testprovider'
}
expect(response.status).to eq(404)
authenticator.can_revoke = true
post "/u/#{user.username}/preferences/revoke-account.json", params: {
provider_name: 'testprovider'
}
expect(response.status).to eq(200)
end
it 'works' do
authenticator.can_revoke = true
post "/u/#{user.username}/preferences/revoke-account.json", params: {
provider_name: 'testprovider'
}
expect(response.status).to eq(200)
end
end
end
end
describe '#revoke_auth_token' do
context 'while logged in' do
before do
2.times { sign_in(user) }
end
it 'logs user out' do
ids = user.user_auth_tokens.order(:created_at).pluck(:id)
post "/u/#{user.username}/preferences/revoke-auth-token.json",
params: { token_id: ids[0] }
expect(response.status).to eq(200)
user.user_auth_tokens.reload
expect(user.user_auth_tokens.count).to eq(1)
expect(user.user_auth_tokens.first.id).to eq(ids[1])
end
it 'does not let user log out of current session' do
token = UserAuthToken.generate!(user_id: user.id)
env = Rack::MockRequest.env_for("/", "HTTP_COOKIE" => "_t=#{token.unhashed_auth_token};")
Guardian.any_instance.stubs(:request).returns(Rack::Request.new(env))
post "/u/#{user.username}/preferences/revoke-auth-token.json", params: { token_id: token.id }
expect(response.status).to eq(400)
end
it 'logs user out from everywhere if token_id is not present' do
post "/u/#{user.username}/preferences/revoke-auth-token.json"
expect(response.status).to eq(200)
expect(user.user_auth_tokens.count).to eq(0)
end
end
end
describe '#list_second_factors' do
before do
sign_in(user)
end
context 'when SSO is enabled' do
before do
SiteSetting.sso_url = 'https://discourse.test/sso'
SiteSetting.enable_sso = true
end
it 'does not allow access' do
post "/u/second_factors.json"
expect(response.status).to eq(404)
end
end
context 'when local logins are not enabled' do
before do
SiteSetting.enable_local_logins = false
end
it 'does not allow access' do
post "/u/second_factors.json"
expect(response.status).to eq(404)
end
end
context 'when the site settings allow second factors' do
before do
SiteSetting.enable_local_logins = true
SiteSetting.enable_sso = false
end
context 'when the password parameter is not provided' do
let(:password) { '' }
before do
post "/u/second_factors.json", params: { password: password }
end
it 'returns password required response' do
expect(response.status).to eq(200)
response_body = JSON.parse(response.body)
expect(response_body['password_required']).to eq(true)
end
end
context 'when the password is provided' do
let(:user) { Fabricate(:user, password: '8555039dd212cc66ec68') }
context 'when the password is correct' do
let(:password) { '8555039dd212cc66ec68' }
it 'returns a list of enabled totps and security_key second factors' do
totp_second_factor = Fabricate(:user_second_factor_totp, user: user)
security_key_second_factor = Fabricate(:user_security_key, user: user, factor_type: UserSecurityKey.factor_types[:second_factor])
post "/u/second_factors.json", params: { password: password }
expect(response.status).to eq(200)
response_body = JSON.parse(response.body)
expect(response_body['totps'].map { |second_factor| second_factor['id'] }).to include(totp_second_factor.id)
expect(response_body['security_keys'].map { |second_factor| second_factor['id'] }).to include(security_key_second_factor.id)
end
end
context 'when the password is not correct' do
let(:password) { 'wrongpassword' }
it 'returns the incorrect password response' do
post "/u/second_factors.json", params: { password: password }
response_body = JSON.parse(response.body)
expect(response_body['error']).to eq(
I18n.t("login.incorrect_password")
)
end
end
end
end
end
describe '#feature_topic' do
fab!(:topic) { Fabricate(:topic) }
fab!(:other_topic) { Fabricate(:topic) }
fab!(:other_user) { Fabricate(:user) }
fab!(:private_message) { Fabricate(:private_message_topic, user: other_user) }
fab!(:category) { Fabricate(:category_with_definition) }
describe "site setting enabled" do
before do
SiteSetting.allow_featured_topic_on_user_profiles = true
end
it 'requires the user to be logged in' do
put "/u/#{user.username}/feature-topic.json", params: { topic_id: topic.id }
expect(response.status).to eq(403)
end
it 'returns an error if the user tries to set for another user' do
sign_in(user)
topic.update(user_id: other_user.id)
put "/u/#{other_user.username}/feature-topic.json", params: { topic_id: topic.id }
expect(response.status).to eq(403)
end
it 'returns an error if the topic is a PM' do
sign_in(other_user)
put "/u/#{other_user.username}/feature-topic.json", params: { topic_id: private_message.id }
expect(response.status).to eq(403)
end
it "returns an error if the topic is not visible" do
sign_in(user)
topic.update_status('visible', false, user)
put "/u/#{user.username}/feature-topic.json", params: { topic_id: topic.id }
expect(response.status).to eq(403)
end
it "returns an error if the topic's category is read_restricted" do
sign_in(user)
category.set_permissions({})
topic.update(category_id: category.id)
put "/u/#{other_user.username}/feature-topic.json", params: { topic_id: topic.id }
expect(response.status).to eq(403)
end
it 'sets featured_topic correctly for user created topic' do
sign_in(user)
topic.update(user_id: user.id)
put "/u/#{user.username}/feature-topic.json", params: { topic_id: topic.id }
expect(response.status).to eq(200)
expect(user.user_profile.featured_topic).to eq topic
end
it 'sets featured_topic correctly for non-user-created topic' do
sign_in(user)
put "/u/#{user.username}/feature-topic.json", params: { topic_id: other_topic.id }
expect(response.status).to eq(200)
expect(user.user_profile.featured_topic).to eq other_topic
end
describe "site setting disabled" do
before do
SiteSetting.allow_featured_topic_on_user_profiles = false
end
it "does not allow setting featured_topic for user_profiles" do
sign_in(user)
topic.update(user_id: user.id)
put "/u/#{user.username}/feature-topic.json", params: { topic_id: topic.id }
expect(response.status).to eq(403)
end
end
end
end
describe '#clear_featured_topic' do
fab!(:topic) { Fabricate(:topic) }
fab!(:other_user) { Fabricate(:user) }
it 'requires the user to be logged in' do
put "/u/#{user.username}/clear-featured-topic.json"
expect(response.status).to eq(403)
end
it 'returns an error if the the current user does not have access' do
sign_in(user)
topic.update(user_id: other_user.id)
put "/u/#{other_user.username}/clear-featured-topic.json"
expect(response.status).to eq(403)
end
it 'clears the user_profiles featured_topic correctly' do
sign_in(user)
topic.update(user: user)
put "/u/#{user.username}/clear-featured-topic.json"
expect(response.status).to eq(200)
expect(user.user_profile.featured_topic).to eq nil
end
end
def create_second_factor_security_key
sign_in(user)
stub_secure_session_confirmed
post "/u/create_second_factor_security_key.json"
end
def stub_secure_session_confirmed
UsersController.any_instance.stubs(:secure_session_confirmed?).returns(true)
end
end